The sample code for this post can be found here.
In the last three posts (part 1, part 2, part 3) we developed our Songster application. Due to new and changing requirements, our code has become quite complex and convoluted at places. There are also some issues with the app. For example, the app does not deal well with device rotations. Let’s clean up our code and fix these issues.
Access Repository through UseCase
First we’re going to remove all direct interactions with the SearchRepository from the SearchPresenter. Instead, the presenter will access the Repository through the UseCase. This simplifies our Presenter because it no longer needs to implement all of the Repository’s listener methods.
Save View Model in Fragment’s instance state
In my previous post I mentioned that state restoration is one of the major benefits of a view model. I illustrated that with the example of restoring the fragment’s state after we return to it through a fragment transaction. The measures we took to deal with that will not suffice when we want to restore the state after an orientation change, however. In order to gracefully recreate our view’s state after an orientation change, we need to do a few additional things. But thanks to our view model, those additional steps are a breeze.
First to save the instance state, we simply override onSaveInstanceState():
@Override public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(KEY_VIEW_MODEL, mViewModel); }
We store our view model (which is Parcelable) in the bundle.
To restore our state we override the onActivityCreated() method. This method is called after onCreateView() and receives the restored state as a bundle. We can then retrieve our view model from the bundle and pass it to our presenter.
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (savedInstanceState != null) { //probably orientation change mViewModel = savedInstanceState.getParcelable(KEY_VIEW_MODEL); } else { if(mViewModel == null) { mViewModel = new SearchViewModel(); } } mPresenter = new SearchPresenter(mViewModel, new SearchMockDataRepository(), new UserDataInMemoryRepository(), this); configureRecyclerView(); mPresenter.present(); }
Note that we moved the creation of our presenter from onCreate() to this method because this method gets called later in the fragment’s lifecycle and because our presenter needs our initialised view model. Furthermore, note that we moved the call to the presenter’s present() method from onCreateView() to this method as well. This is also because onActivityCreated() is called after onCreateView(). See the Android developer documentation for more information on the fragment lifecycle.
In our MainActivity’s onCreate we need to make a few minor changes as well:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(savedInstanceState == null) { Fragment mainFragment = getSupportFragmentManager(). findFragmentByTag(TAG_MAIN_FRAGMENT); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); if (mainFragment != null) { ft.replace(R.id.fragmentContainer, MainFragment.getInstance(), TAG_MAIN_FRAGMENT); } else { ft.add(R.id.fragmentContainer, MainFragment.getInstance(), TAG_MAIN_FRAGMENT); } ft.commit(); } }
The two major things going on here are very important in making state restoration work properly. The first thing I will explain is the adding or replacing of our fragment. Instead of simply adding our fragment every time, we first check if it’s already been added. If so, we simply replace it. Adding the fragment every time will eventually lead to overlapping fragments.
The second thing we need to do to properly handle state restoration is we need to check if our instance state is null and only add or replace the fragment if there is no instance state. The reason why is because when the device is rotated, for instance, the fragment’s onCreate() is first called. The fragment then receives the instanceState bundle. Now the activity’s onCreate() method is called. If we were then to add or replace our fragment, the fragment’s onCreate() method would be called again but this time without a savedInstanceState. With no savedInstanceState, we would create a new view model and our restored view model from the previous call to the fragment’s onCreate() would be lost.
Conclusion
With these changes our app becomes a lot more stable and also a bit easier to maintain. With the help of our presenter we are able to clearly and cleanly separate the Android world from our java world. We are also able to test drive the logic without having to worry about the details of Android. Our view model models the state of our view and deals with any logic required to model and modify that state. Our view model can also be parcelled into a bundle and restored when the fragment is recreated. The UseCase contains the more complex business logic that is not always directly related to the UI. Finally our repository abstracts the data access. With these tools and patterns, making a scalable Android app that is also testable is much easier.
Pingback: Noser Blog Data binding and Model-View-ViewModel in Android - Noser Blog