I am trying to populate some data on a RecyclerView
. The data is obtained in one of the ways:
Retrofit
and RxJava2
), persist the data to local database (using Room
), send the data to the View
(in my case it is a Fragment
).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:
RecyclerViewAdapter
, the screen is blank. No data is shown.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)
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.