Scope
In part 6, I rewrote the movie search app in MVVM using LiveData and ViewModel architectural components from Jetpack. The app looks good in portrait mode, but there is an issue when screen rotates which is due to what I call an incorrect use of LiveData in part 6. This was deliberately made to draw your attention to a delicate point on where to use LiveData or RxJava.
In this part, after explaining the error, I try to shed light on how I believe LiveData and ViewModel shall be used together in android apps. The branch mvvm-architecture_component-liveData&viewmodel-revised on the series repo on Github reflects the fix to this error.
By the end of this part, I hope you will gain a clear understanding of where to use each.
Let’s face the issue
The issue happens when user clicks on an item on the list, hits the back button on the detail screen and returns to the list. Now when rotating the screen, the immediate screen shown is the same detail screen, this time without clicking on the list.
How does this happen?
Well, let’s see the code:
This method from MainViewModel is called when there is a click on a list item:
1 2 3 |
fun doOnItemClick(position: Int) { itemObservable.value = entityList[position] } |
where val itemObservable = MutableLiveData<MainModel.ResultEntity>() is a LiveData .
So click on a list item causes LiveData to post a new value which is then observed in the MainActivity :
1 |
mMainViewModel.getItemObservable().observe(this, Observer { goToDetailActivity(it!!) }) |
which navigates to the DetailActivity .
Here, we did not click on any list item after screen rotation. We also know that Activity is destroyed and recreated on such configuration change. Hence there shouldn’t be any track of our previous clicks since we did not manually stored them before rotation. So why does this happen?
The answer lies in the mutual action of ViewModel and LiveData in Jetpack architecture components. In fact, by assigning a MainViewModel extending ViewModel architecture component to our MainActivity :
1 |
mMainViewModel = ViewModelProviders.of(this, MainViewModelFactory(SchedulersWrapper())).get(MainViewModel::class.java) |
we tell the operating system that we do not want it to create a new instance of MainViewModel every time MainActivity is recreated. So the system uses our existing instance of MainViewModel before and after rotation, making it a singleton.
The other thing that the OS does is that it reposts the last value posted to each LiveData inside the ViewModel inheriting from Jetpack ViewModel before screen rotation. This causes all three LiveData s inside our MainViewModel to be posted their last values before rotation automatically from the OS:
1 2 3 |
val resultListObservable = MutableLiveData<List<String>>() val resultListErrorObservable = MutableLiveData<HttpException>() private val itemObservable = MutableLiveData<MainModel.ResultEntity>() |
The first LiveData ( resultListObservable ) makes its observer in MainActivity to show the same list of search results before screen rotation to the user. Alternatively, if there was an error before rotation, the observer to the second LiveData ( resultListErrorObservable ) shows the same error page. Finally, the third LiveData ( itemObservable ) receives the same item clicked before rotation by the system causing its observer to show DetailActivity .
Is this a negative thing with LiveData? Should we avoid LiveData?
Absolutely not. There is nothing wrong with LiveData , the problem is with our misusing it here.
In fact, LiveData together with the ViewModel from Jetpack architecture components were introduced to provide coherent and consistent UI to the user before and after configuration change by storing view-related data in LiveDatas inside the ViewModel and emitting them again on Activity recreation. This causes the different view components of the app to receive the exact same data before and after configuration change, rendering a seamless user experience.
On the other hand, LiveData handles lifecycle management and leak issues itself so we don’t have to care about disposing our observers in, for example, onDestroy() method of the Activity .
If we wanted to use RxJava instead of LiveData for the same functionality above, we had to use BehaviorSubject . In other words, LiveData acts like a simplified version of BehaviorSubject in RxJava which emits the most recent item before subscribing to it and all subsequent items to the observer.
So any attempt at changing the default implementation of LiveData , like the one I saw on a forum on extending it to change the default behaviour to prevent it from posting values on subscription, would not probably be inline with why LiveData was created in the first place.
In my opinion, LiveData should only be used for populating the view elements with the same data we want to show before and after configuration change. Anything beyond, such as trying to handle streams, making it work as RxJava , etc, is misusing LiveData.
Now how to fix this example?
There are two ways:
1- [Simple way] Change the structure of the the code in a way that there is no need to post a value to LiveData in the ViewModel to handle button click:
For this, one line in onBindViewHolder of the listAdapter shall be changed from
1 |
holder.itemView.setOnClickListener { mOnClick(position) } |
to
1 |
holder.itemView.setOnClickListener { mOnClick(mList[position]) } |
Then inside MainActivity , the following must be changed from
1 2 3 |
addressAdapter setItemClickMethod { mMainViewModel.doOnItemClick(it) } |
to
1 2 3 |
addressAdapter setItemClickMethod { goToDetailActivity(it) } |
thereby bypassing the MainViewModel method call.
2- [More complex way] Use PublishSubject instead of LiveData in MainViewModel , so that it does not emit the items before subscriptions to the observer:
1 |
val itemObservable = PublishSubject.create<MainModel.ResultEntity>() |
Then in MainActivity :
1 |
mMainViewModel.itemObservable.subscribe { goToDetailActivity(it!!) } |
I have used the second way for fixing this issue, as found in ‘branch mvvm-architecture_component-liveData&viewmodel-revised’.
Here is the result after applying the fix:
What is the takeaway?
Even though LiveData uses the observer pattern for notifying the observers, it should not be compared with RxJava. In edge case, it can be considered a simplified version of BehaviorSubject in RxJava with internal lifecycle management. Its function is to populate the view elements with their respective data before and after configuration change as well as to stop listening to this data when Activity no longer exists (such as onDestroy ). In doing so, ViewModel and LiveData from Jetpack architecture components work like pancake and cream (you may read peanut butter and jelly).
So it is great to use LiveData together with ViewModel in modern app development, but be ware of misusing LiveData . RxJava is always there for you.
Hi,
I read the whole series and have to thank you for finding time to write those complex things in an easy-digestable way. It was really helpful.
Hello,
Thanks for the great article. What about fragments? In fragments (because of their lifecycle) by every transition, the LiveData onChange method will call. Is it suitable to use rxJava rather than LiveData in this situation?