Search code examples
androidandroid-recyclerviewrx-java2android-roomandroid-mvp

RecyclerView not showing anything even after receiving data


I am trying to populate some data on a RecyclerView. The data is obtained in one of the ways:

  1. Request data from an API if internet is present (using Retrofit and RxJava2), persist the data to local database (using Room), send the data to the View (in my case it is a Fragment).
  2. If no internet then get data from the local database and send it to the View.

I'm using MVP as the application architecture and I'm using ViewModels to deal with the data handling for the UI. I am using ButterKnife to do the view finding.

The Presenter calls the Repository, which gets the data based on the conditions specified above and then the data is sent to the Views by the Presenter where the ViewModel intercepts the data and does the necessary steps to pass the data to the UI elements (the RecyclerView adapter update).

My problem is:

  1. Even when I see in the debugger that the data is reaching to RecyclerViewAdapter, the screen is blank. No data is shown.
  2. The result from the local database throws IllegalStateException, saying that local db cannot be accessed from the main thread, even when I subscribeOn(Schedulers.io()) when I'm calling the function to return the data from local db, which return a Maybe<>.

My code is as follows:
The repository:

public class CountriesListApiRepository {
    private final IRestServiceDataFetcher restServiceDataFetcher; // Retrofit interface to get data from API

    @Inject
    public CountriesListApiRepository(IRestServiceDataFetcher restServiceDataFetcher) {
        this.restServiceDataFetcher = restServiceDataFetcher;
    }

    public Maybe<List<CountriesFullEntity>> getCountriesFromApi() {
        return restServiceDataFetcher
                .getListOfCountriesData()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io());
    }
}

public class CountriesListLocalDbRepository {
    private final CountriesLocalDb countriesLocalDb;

    @Inject
    CountriesListLocalDbRepository(CountriesLocalDb countriesLocalDb) {
        this.countriesLocalDb = countriesLocalDb;
    }

    Maybe<List<CountriesFullEntity>> getCountriesFromLocalDb() {
        return this.countriesLocalDb
                .getCountriesLocalDbDAO()
                .getCountriesList()
                .observeOn(Schedulers.io())
                .subscribeOn(Schedulers.io());
    }

    void updateLocalDb(List<CountriesFullEntity> updatedCountriesList) {
        countriesLocalDb.getCountriesLocalDbDAO().insertAllCountries(updatedCountriesList);

    }
}

public class CountriesListRepository {
    private final CountriesListApiRepository countriesListApiRepository;
    private final CountriesListLocalDbRepository countriesListLocalDbRepository;
    private static final String TAG_CountriesListRepository = "CountriesListRepository";

    @Inject
    public CountriesListRepository(CountriesListApiRepository countriesListApiRepository,
                                   CountriesListLocalDbRepository countriesListLocalDbRepository) {
        this.countriesListApiRepository = countriesListApiRepository;
        this.countriesListLocalDbRepository = countriesListLocalDbRepository;
    }

    public Maybe<List<CountriesFullEntity>> getCountries() {
        return this.countriesListApiRepository
                .getCountriesFromApi()
                .doOnSuccess(this.countriesListLocalDbRepository::updateLocalDb)
                .doOnError(throwable -> {
                    Log.d(TAG_CountriesListRepository,throwable.getMessage());
                    throwable.printStackTrace(); })
                .onErrorResumeNext(this.countriesListLocalDbRepository.getCountriesFromLocalDb());
    }
}  


My Presenter:

public class BasePresenter<T extends BaseView> {
    @Inject
    T injectedView;

    private static final String TAG_BASE_PRESENTER = "BasePresenter";

    T getInjectedView() {
        return injectedView;
    }

    <V> void subscribeToObserver(Maybe<V> flowable, MaybeObserver<V> maybeObserver) {
        flowable.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe(subscription -> Log.d(TAG_BASE_PRESENTER,"Subscribed"))
                .doOnSuccess(v -> Log.d(TAG_BASE_PRESENTER,"onNext() completed"))
                .subscribe(maybeObserver);
    }
}  

public class CountriesListPresenter extends BasePresenter<CountriesListView>
        implements MaybeObserver<List<CountriesFullEntity>> {

    private static final String TAG_ListPresenter = "CountriesListPresenter";

    private final CountriesListRepository countriesListRepository;

    private Disposable disposable;

    @Inject
    public CountriesListPresenter(CountriesListRepository countriesListRepository) {
        this.countriesListRepository = countriesListRepository;
    }

    public void getCountries() {
        Maybe<List<CountriesFullEntity>> listOfCountriesData = this.countriesListRepository
                .getCountries();
        subscribeToObserver(listOfCountriesData, this);
    }

    public void updateCountriesList(@NonNull Boolean isInternetThere) {
        Maybe<List<CountriesFullEntity>> newCountriesFullData = this.countriesListRepository
                .updateCountriesList(isInternetThere);
        subscribeToObserver(newCountriesFullData,this);
    }

    @Override
    public void onSubscribe(Disposable d) {
        this.disposable = d;
    }

    @Override
    public void onSuccess(List<CountriesFullEntity> countriesFullEntities) {
        Log.d(TAG_ListPresenter,"Executing onNext()");
        getInjectedView().onLoadCountriesDataFull(countriesFullEntities);
    }

    @Override
    public void onError(Throwable e) {
        Log.d(TAG_ListPresenter,"Executing onError()",e);
        getInjectedView().onErrorEncountered(e.getMessage());
    }

    @Override
    public void onComplete() {
        if(!disposable.isDisposed()) disposable.dispose();
    }
}  


My Views:

public class MainActivity extends AppCompatActivity implements IFragmentToFragmentMediator, SwipeRefreshLayout.OnRefreshListener {

    public static final String TAG_MAIN_ACTIVITY = "MainActivity";

    public static final String TAG_LIST_FRAGMENT = "COUNTRY_LIST";
    public static final String TAG_DETAILS_FRAGMENT = "COUNTRY_DETAILS";

    @BindView(R.id.MainActSwipeRefreshLayout)
    SwipeRefreshLayout swipeRefreshLayout;

    private Unbinder unbinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        unbinder = ButterKnife.bind(this);
        swipeRefreshLayout.setOnRefreshListener(this);
        Configuration config = getResources().getConfiguration();
        Log.d(TAG_MAIN_ACTIVITY,"Main Activity View Created");
        if(config.smallestScreenWidthDp<600){
            CountriesListFrag countriesListFrag = new CountriesListFrag();
            conductFragmentTransaction(countriesListFrag,TAG_LIST_FRAGMENT, false, true);
            Log.d(TAG_MAIN_ACTIVITY,"Main Activity < 600 and portrait");
        }
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        FragmentManager fm = getSupportFragmentManager();
        if(getSupportFragmentManager().getBackStackEntryCount() > 1) {
            Fragment f = fm.findFragmentByTag(TAG_DETAILS_FRAGMENT);
            if (f instanceof  CountryDetailsFrag) fm.popBackStack();
            else this.finish();
        }
    }

    @Override
    public void onRefresh() {
        // TODO : Code the refreshing data callback here
        Fragment countryListFragment = returnNonNullRunningFragmentByTagName(TAG_LIST_FRAGMENT);
        Fragment countryDetailsFragment = returnNonNullRunningFragmentByTagName(TAG_DETAILS_FRAGMENT);
        makeViewsSignalUpdateOfData((CountriesListFrag)countryListFragment);
        makeViewsSignalUpdateOfData((CountryDetailsFrag)countryDetailsFragment);
    }

    @Override
    public void invokeDetailsFragmentOnListItemCLickedInListFragment(CountriesFullEntity countriesFullEntity) {
        CountryDetailsFrag countryDetailsFrag = CountryDetailsFrag.newInstance();
        Bundle data = new Bundle();
        data.putSerializable("data",countriesFullEntity);
        countryDetailsFrag.setArguments(data);
        conductFragmentTransaction(countryDetailsFrag, TAG_DETAILS_FRAGMENT, false, true);
    }

    @Override
    public void invokeDetailsFragmentOnListItemClickedInListFragmentViewModel() {

        CountryDetailsFrag countryDetailsFrag =
                (CountryDetailsFrag) getSupportFragmentManager().findFragmentByTag(TAG_DETAILS_FRAGMENT);

        if(countryDetailsFrag!=null && countryDetailsFrag.isVisible())
        conductFragmentTransaction(countryDetailsFrag, TAG_DETAILS_FRAGMENT, true, true);
    }

    private void makeViewsSignalUpdateOfData(BaseView view) {
        if(view != null) view.onPerformUpdateAction();
    }

    private Fragment returnNonNullRunningFragmentByTagName(String tagName) {
        Fragment fragmentForProcessing = getSupportFragmentManager().findFragmentByTag(tagName);
        if(fragmentForProcessing!=null && fragmentForProcessing.isVisible()) return fragmentForProcessing;
        return null;
    }

    private void conductFragmentTransaction(Fragment targetFragment, String tag, boolean addOrReplaceFlag, boolean backStackFlag) {
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        if(addOrReplaceFlag)fragmentTransaction.replace(R.id.fragmentCanvas,targetFragment,tag);
        else fragmentTransaction.add(R.id.fragmentCanvas,targetFragment,tag);
        if(backStackFlag)fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbinder.unbind();
    }
}  

public class CountriesListFrag extends Fragment implements CountriesListView {

    public static final String TAG_LIST_FRAGMENT = "ListFragment";

    @Inject
    Picasso picasso;
    @Inject
    CountriesListPresenter countriesListPresenter;

    private IFragmentToFragmentMediator listeningActivity;
    private Frag2FragCommViewModel frag2FragCommViewModel;

    @BindView(R.id.CountriesEntireHolderRV)
    RecyclerView countriesEntireHolderRV;
    private CountriesListRecyclerViewAdapter countriesListRecyclerViewAdapter;
    private Unbinder unbinder;

    public CountriesListFrag() {
    }

    @Override
    public void onAttach(Context context) {
        DaggerCountryComponents
                .builder()
                .appComponents(((CentralApplication)context.getApplicationContext())
                        .getAppComponents())
                .countryModule(new CountryModule(this))
                .build()
                .inject(this);
        super.onAttach(context);
        try{
            this.listeningActivity = (MainActivity) context;

            this.frag2FragCommViewModel = ViewModelProviders
                    .of((MainActivity) context)
                    .get(Frag2FragCommViewModel.class);
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement IFragmentToFragmentMediator");
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_countries_list, container, false);
        if(v!=null) this.unbinder = ButterKnife.bind(this,v);

        // Inflate the layout for this fragment
        initialize();
        this.countriesListPresenter.getCountries();
        setUpListItemClickListener();

        frag2FragCommViewModel
                .getLiveDataListOfCountriesData()
                .observe(this,countriesFullEntities ->
                        countriesListRecyclerViewAdapter.setCountriesFullEntityList(countriesFullEntities));
        Log.d(TAG_LIST_FRAGMENT,"List Fragment View Created");

        return v;
    }

    @Override
    public void onLoadCountriesDataFull(List<CountriesFullEntity> countriesFullData) {
//        this.countriesListRecyclerViewAdapter.setCountriesFullEntityList(countriesFullData);
        Log.d(TAG_LIST_FRAGMENT,"Data Received in List Fragment");
        Log.d(TAG_LIST_FRAGMENT,countriesFullData.get(0).getName());
        this.frag2FragCommViewModel.setListOfCountriesData(countriesFullData);
    }

    @Override
    public void onErrorEncountered(String errorMessage) {
        Toast.makeText(getContext(), errorMessage,Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onPerformUpdateAction() {
        callToPresenterToUpdateListOfCountriesOnInternetPresent(this.getContext());
    }

    private void initialize() {
        this.countriesEntireHolderRV = new RecyclerView(this.getContext());
        this.countriesListRecyclerViewAdapter = new CountriesListRecyclerViewAdapter(new ArrayList<>(),
                LayoutInflater.from(this.getContext()), this.picasso);

        this.countriesEntireHolderRV.setLayoutManager(new LinearLayoutManager(this.getContext()));
        this.countriesEntireHolderRV
                .addItemDecoration(new DividerItemDecoration(this.countriesEntireHolderRV.getContext()
                        ,DividerItemDecoration.VERTICAL));
        this.countriesEntireHolderRV.setAdapter(this.countriesListRecyclerViewAdapter);
        Log.d(TAG_LIST_FRAGMENT,"List Fragment init");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        this.unbinder.unbind();
    }
}  

public class CountriesListRecyclerViewAdapter extends RecyclerView.Adapter<CountriesListRecyclerViewAdapter.CountriesListViewHolder>{
    private List<CountriesFullEntity> countriesFullEntityList;
    private final LayoutInflater layoutInflater;
    private final Picasso picasso;
    private CountriesListRVClickListener countriesListRVClickListener;
    private int currentPosition;

    CountriesListRecyclerViewAdapter(List<CountriesFullEntity> countriesFullEntityList, LayoutInflater layoutInflater, Picasso picasso) {
        this.countriesFullEntityList = countriesFullEntityList;
        this.layoutInflater = layoutInflater;
        this.picasso = picasso;
        this.currentPosition = 0;
    }

    @NonNull
    @Override
    public CountriesListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new CountriesListViewHolder(this.layoutInflater.inflate(R.layout.countries_list_layout,parent,false));
    }

    @Override
    public void onBindViewHolder(@NonNull CountriesListViewHolder holder, int position) {
        CountriesFullEntity countriesFullEntity = countriesFullEntityList.get(position);
        picasso.load(countriesFullEntity.getFlag()).into(holder.countryIcon);
        holder.nameOfCountry.setText(countriesFullEntity.getName());
    }

    @Override
    public int getItemCount() {
        return this.countriesFullEntityList.size();
    }

    void setItemClickListener(CountriesListRVClickListener itemClickListener) {
        this.countriesListRVClickListener = itemClickListener;
    }

    CountriesFullEntity getCountriesFullEntityAtPosition(int position) {
        return this.countriesFullEntityList.get(position);
    }

    void setCountriesFullEntityList(List<CountriesFullEntity> countriesFullEntityList) {
        this.countriesFullEntityList = countriesFullEntityList;
        notifyDataSetChanged();
    }

    int getCurrentPosition() {
        return currentPosition;
    }

    void setCurrentPosition(int currentPosition) {
        this.currentPosition = currentPosition;
    }

    class CountriesListViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        @BindView(R.id.NameOfCountry)
        TextView nameOfCountry;
        @BindView(R.id.CountryIcon)
        ImageView countryIcon;
        CountriesListViewHolder(View itemView) {
            super(itemView);
        }

        @Override
        public void onClick(View v) {
            countriesListRVClickListener.onListItemClicked(v,getAdapterPosition());
        }
    }
}  


My DAOs:

@Dao
public interface CountriesLocalDbDAO {
    @Query("SELECT * FROM Countries")
    Maybe<List<CountriesFullEntity>> getCountriesList();

    @Query("SELECT * FROM Countries WHERE name LIKE :countryName")
    Maybe<CountriesFullEntity> getCountryByName(String countryName);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAllCountries(List<CountriesFullEntity> countryList);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertSingleCountry(CountriesFullEntity singleCountry);

    @Delete
    void deleteSingleCountry(CountriesFullEntity singleCountry);

    @Delete
    void endOfTheWorld(List<CountriesFullEntity> theWorld);
}  

public interface IRestServiceDataFetcher {
    @GET("all")
    Maybe<List<CountriesFullEntity>> getListOfCountriesData();
    @GET("name/{name}")
    Maybe<CountriesFullEntity> getParticularCountry(@Path("name") String name);
}


My layouts:

ActivityMainLayout:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="@+id/fragmentCanvas"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/MainActSwipeRefreshLayout"
        android:layout_height="20dp"
        android:layout_width="match_parent">
    </android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>

FragmentListLayout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.countrieslist.CountriesListFrag">

    <!-- TODO: Update blank fragment layout -->


    <android.support.v7.widget.RecyclerView
        android:id="@+id/CountriesEntireHolderRV"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>  

RecyclerViewViewHolderLayout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/CountryIcon"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:contentDescription="@string/app_name"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_launcher_background" />

    <TextView
        android:id="@+id/NameOfCountry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="@string/app_name"
        android:textSize="40sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/CountryIcon"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>


My app build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "shankhadeepghoshal.org.countrieslistapp"
        minSdkVersion 14
        //noinspection OldTargetApi
        targetSdkVersion 27
        multiDexEnabled true
        versionCode 1
        versionName "1.0"
        vectorDrawables.useSupportLibrary = true
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        javaCompileOptions {
            annotationProcessorOptions {
                includeCompileClasspath = true
            }
        }
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -frtti -fexceptions"
            }
        }
        // multiDexEnabled true
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
    }
    buildToolsVersion '27.0.3'
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation 'com.google.dagger:dagger:2.16'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.16'
    implementation 'com.google.dagger:dagger-android:2.16'
    implementation 'com.google.dagger:dagger-android-support:2.16'
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.16'

    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.0'

    implementation 'com.squareup.picasso:picasso:2.71828'

    implementation(
            [group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.6'],
            [group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'],
            [group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6']
    )

    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-jackson:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-java8:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.5.0'

    compileOnly 'org.projectlombok:lombok:1.18.2'
    annotationProcessor 'org.projectlombok:lombok:1.18.2'

    def room_version = "1.1.1"
    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version"
    implementation "android.arch.persistence.room:rxjava2:$room_version"
    implementation "android.arch.persistence.room:guava:$room_version"
    testImplementation "android.arch.persistence.room:testing:$room_version"

    def lifecycle_version = "1.1.1"
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"
    implementation "android.arch.lifecycle:runtime:$lifecycle_version"
    implementation "android.arch.lifecycle:common-java8:$lifecycle_version"
    implementation "android.arch.lifecycle:reactivestreams:$lifecycle_version"
    testImplementation "android.arch.core:core-testing:$lifecycle_version"

    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

    implementation 'com.android.support:design:27.1.1'
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.android.support:recyclerview-v7:27.1.1'
    implementation 'com.android.support:support-compat:27.1.1'
    implementation 'com.android.support:exifinterface:27.1.1'
    implementation 'com.android.support:multidex:1.0.3'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test:rules:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

UPDATE:

I did what @Ben-P suggested in his answer. But now my RecycleViewAdapter is giving me errors.
I am getting an IllegalArgumentException - Target must be null on this line : picasso.load(countriesFullEntity.getFlag()).into(holder.countryIcon); in onBindViewHolder(holder,position)


Solution

  • In your fragment, you have this code:

    private void initialize() {
        this.countriesEntireHolderRV = new RecyclerView(this.getContext());
        ...
    }
    

    Earlier, in onCreateView(), you call ButterKnife.bind()... which means that the above code is reassigning countriesEntireHolderRV to a new reference. This new RecyclerView is never added to your Fragment's view (and probably wouldn't be visible anyway, since the LinearLayout already has a child taking up all the visible space), so all of your operations work with a RecyclerView that the user can't see.

    Just delete that line from initialize(). Then you'll be working with the RecyclerView in your layout file, and everything should appear.