Make it work

Save ViewModel state between fragments with navigation

Overview

Today I was a little stuck because I couldn’t understand why my ViewModel is created every time I switch to another fragment with the bottom navigation bar. In the official Google guides, you can find this diagram 

with the comment

The scope of an activity or fragment goes from created to finished (or terminated), which you must not confuse with destroyed. Remember that when a device is rotated, the activity is destroyed but any instances of ViewModel associated with it are not.

 So, if I use

homeViewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java) it should return me an existing instance of ViewModel, instead of creating new one, right? Well, in the case below it doesn’t work like that, and if you straggling the same problem and don’t want to waste much time to read the whole article, just give a try this code val homeViewModel: HomeViewModel by navGraphViewModels(R.id.mobile_navigation)

Simple application

Let’s create a simple application with the Bottom Navigation Activity in the Android Studio. In the fragment_home.xml add the code:

fragment_home.xml
  1. <TextView
  2. android:id="@+id/name_text"
  3. android:layout_width="wrap_content"
  4. android:layout_height="wrap_content"
  5. android:layout_marginTop="64dp"
  6. android:text="Anetka"
  7. app:layout_constraintEnd_toEndOf="parent"
  8. app:layout_constraintStart_toStartOf="parent"
  9. app:layout_constraintTop_toBottomOf="@+id/text_home" />
  10. <Button
  11. android:id="@+id/set_name_button"
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:layout_marginTop="32dp"
  15. android:text="@string/set_name"
  16. app:layout_constraintEnd_toEndOf="parent"
  17. app:layout_constraintStart_toStartOf="parent"
  18. app:layout_constraintTop_toBottomOf="@+id/name_text" />
  <TextView
        android:id="@+id/name_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="64dp"
        android:text="Anetka"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text_home" />

    <Button
        android:id="@+id/set_name_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="@string/set_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/name_text" />

And then in HomeFragment.kt

HomeFragment.kt
  1. val nameText: TextView = root.findViewById(R.id.name_text)
  2. val setNameButton: Button = root.findViewById(R.id.set_name_button)
  3. setNameButton.setOnClickListener { button ->
  4. nameText.text = "Roman"
  5. }
val nameText: TextView = root.findViewById(R.id.name_text)
val setNameButton: Button = root.findViewById(R.id.set_name_button)
setNameButton.setOnClickListener { button ->
	nameText.text = "Roman"
}

So it’s pretty simple. We’ve got a TextView with name Anetka and after the button click, we want to change it to Roman.

As we can see after rotating the device Anetka is coming back… But we don’t want it! We want to stay with Roman! Of course, you can use the onSaveInstanceState method to achieve the goal, but we can do it better… with the Architecture Components.

HomeViewModel.kt
  1. private val _name = MutableLiveData<String>()
  2. val name: LiveData<String>
  3. get() = _name
  4. init {
  5. Log.d("home-viewmodel", "init home view model!")
  6. _name.value = "Anetka"
  7. }
  8. fun setName(name: String) {
  9. _name.value = name
  10. }
    private val _name = MutableLiveData<String>()
    val name: LiveData<String>
        get() = _name


    init {
        Log.d("home-viewmodel", "init home view model!")
        _name.value = "Anetka"
    }

    fun setName(name: String) {
        _name.value = name
    }
HomeFragment.kt
  1. homeViewModel =
  2. ViewModelProviders.of(this).get(HomeViewModel::class.java)
  3. ....
  4. val nameText: TextView = root.findViewById(R.id.name_text)
  5. val setNameButton: Button = root.findViewById(R.id.set_name_button)
  6. homeViewModel.name.observe(this, Observer { name ->
  7. nameText.text = name
  8. })
  9. setNameButton.setOnClickListener { button ->
  10. homeViewModel.setName("Roman")
  11. }
homeViewModel =
	ViewModelProviders.of(this).get(HomeViewModel::class.java)
....
val nameText: TextView = root.findViewById(R.id.name_text)
val setNameButton: Button = root.findViewById(R.id.set_name_button)
homeViewModel.name.observe(this, Observer { name ->
	nameText.text = name
})
setNameButton.setOnClickListener { button ->
	homeViewModel.setName("Roman")
}

Let’s try it out.

As we can see Roman survived the device rotation, but after switching to another fragment with bottom navigation Anetka is coming back again! In the Logcat log we can see that our ViewModel is initiating every time we use the navigation.

That’s a little surprise, because we use homeViewModel =
ViewModelProviders.of(this).get(HomeViewModel::class.java) to avoid this. However, if we change the homeViewModel initialization to private val homeViewModel: HomeViewModel by navGraphViewModels(R.id.mobile_navigation) it’s going to work as expected.

More information

To read more about Architecture Components and android lifecycle check this pretty good codelabs. For more information about ViewModels and Navigation check this post. The above code is available in the GitHub project

Leave a Reply

Your email address will not be published.