19

I have a problem when using ViewModel and LiveData arch components. When using fragments and rotating the screen, the observer gets triggered...

I tried to move viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) in all the fragment lifecycle methods, but with no success.

My scenario is relatively straightforward:

  1. Login screen with email and password
  2. User clicks on the "login" button
  3. The viewmodel calls the login(email, password) and sets the value of the LiveData object
  4. Just for now simply show a Toast

At this point everything is okay. But when I rotate the screen the Toast appears again without any user interaction.

Do I have to do something in onDestroyView() ?

Thanks in advance!

DV82XL
  • 5,350
  • 5
  • 30
  • 59
Nicolas Jafelle
  • 2,661
  • 2
  • 24
  • 30

2 Answers2

15

Ok Finally found the problem and how to solve. LiveData is not designed for single events. For that reason there is a couple of ways to fix it or handle it, this two links were useful for me:

Jose Alcérreca's post dealing with this problem

Jose Alcérreca's EventObserver

Jose Alcérreca's SingleLiveEvent class

Basically:

In ViewModel:

var eventLiveData: MutableLiveData<Event<ErrorResponse>> = MutableLiveData()

and In Activity or Fragment:

viewModel.eventLiveData.observe(this, EventObserver {
     it?.let {
          shortToast(it.message)
     }
})
Nicolas Jafelle
  • 2,661
  • 2
  • 24
  • 30
  • @Thracian check this! – Nicolas Jafelle Jul 21 '18 at 18:31
  • I'm using **LiveData** as a click listener from my xml > ViewModel > Fragment. If I set the value to null after listening for the click the **LiveData** is observed as null on screen rotate. You have to check if the data is null in the Fragment. This is a hack but works. `fun contentClicked(content: Content) { contentSelected.value = content; contentSelected.value = null; }` – AdamHurwitz Sep 23 '18 at 20:19
5

It's how LiveData and ViewModel works. You are getting same ViewModel with same LiveData and LiveData has previous object, User for instance, with previous credentials when you call ViewModelProviders.of(this).get(MainViewModel::class.java). You can reset User of LiveData onPause()or onStop() to reset it to initial state.

I don't know how you call toast, if you can share your ViewModel and MainActivity i can be more specific.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • I am using the ViewModel as a Presenter. Doing all the business logic there like call the login endpoint, save cache and passing the json data to the livedata.value to trigger the observer. – Nicolas Jafelle Jul 21 '18 at 01:15
  • That's how you should use ViewModel, it's okay. Some examples also use it like presenter sending a weak reference of View as an interface to call Activity methods or use LiveData states to call the methods. What i'm saying is what's good about ViewModel is you get same ViewModel after rotation which has previous states and data. In your case LiveData. You should reset User and call setValue(livedata.getUser()) on MutableLiveData. And do it before rotation onPause() or onStop() – Thracian Jul 21 '18 at 05:17
  • Ok, so is like any other component that you need to take clear or unsubscribe on destroy methods? I thought it handles automatically. – Nicolas Jafelle Jul 21 '18 at 12:34
  • It's not cleared until ViewModel is destroyed. You should check out ViewModel lifecycle. The good thing about ViewModel is to be able to use it after rotation or inside different fragments for inter-fragment communication. You setValue of LiveData in some fragment and observe it on other and it's retains it's value and state until onDestroy() and ViewModel's onCleared() method is called. [This link](https://developer.android.com/topic/libraries/architecture/viewmodel) and [this link](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html#onCleared()) may help. – Thracian Jul 21 '18 at 13:03
  • Thanks! The first link was the first thing to read for this. For example, based on thr first link, let's say that after load all user you show a toast saying "Users sync complete". If you rotate the screen the toast will appear again. I saw that in google docs they validate if the livedata object is null or not. I am creating an inmutable object initializing in the constructor with kotlin. Should I have to use some lazy initialization? – Nicolas Jafelle Jul 21 '18 at 13:39
  • I shared second link to show how you can dispose of subscribers if needed. Image in first link explains the life cycle of ViewModel. I'm glad it helped. Would you mind accepting the answer? – Thracian Jul 21 '18 at 13:43
  • I don't know Kotlin yet. But i've seen they use it with lateinit, but i can't say for sure. You can search ViewModel or MMVM + Kotlin. – Thracian Jul 21 '18 at 13:45
  • If you are saying viewModel.livedata.observe(User user) and checking user null. It's because if you don't setValue(User) of LiveData it's null. That's why they check. You can check out and practice on my this answer [here](https://stackoverflow.com/questions/51288066/how-can-i-do-databinding-with-livedata/51288791#51288791). If you don't use `mUser = new User("User", "asd@abc.com");` ` userMutableLiveData.setValue(mUser);` in that example code will crash because User of LiveData is null – Thracian Jul 21 '18 at 13:49
  • No, User is attached to LiveData when you create LiveData liveUserData = new LiveData<>(); You should set User manually of from Room database or from Web service. That's why you check for null – Thracian Jul 21 '18 at 13:50
  • This solution above worked. Re: *setting the **LiveData** to null `onPause()`*. My solution in the comments above works as well but this is better because you are not checking for *null* state which is a bit of a hack. – AdamHurwitz Sep 23 '18 at 20:28
  • Disregard, both solutions require checking for null state whether you clear the LiveData in the ViewModel like I did above or in the Fragment/Activity lifecyle. As you can see from Jose's Medium both strategies are not recommended. – AdamHurwitz Sep 23 '18 at 20:54