zsmb.coEst. 2017



An Early Look at ViewModel SavedState

2019-03-14 • Márton Braun

If you keep up with the AndroidX release notes - or at least follow people who do - you might have noticed an interesting little bit in the March 13th release:

Now ViewModels can contribute to savedstate. To do that you use newly introduced viewmodel’s factory SavedStateVMFactory and your ViewModel should have a constructor that receives SavedStateHandle object as a parameter.

Saving an application’s state properly so that it survives even the dreaded process death is often discussed in the Android community, and now we’re getting support for it in the latest popular solution for persisting data through configuration changes - ViewModels.

However, the quote above is a little vague on its own, so let’s look at how you can actually use this library with some example code (all of which can be found here in full).

Setting up

Basics first: as the release notes said, your ViewModel class has to have a constructor that takes a single SavedStateHandle parameter.

class MyViewModel(val handle: SavedStateHandle) : ViewModel() 

To instantiate this ViewModel, you’ll need to use a SavedStateVMFactory. This takes your Fragment or Activity as its parameter, which it uses to hook the SavedStateHandle it provides for your ViewModel into the savedInstanceState mechanism.

val factory = SavedStateVMFactory(this)
val viewModel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)

Finally, you’ll want to use the handle to store and fetch data. This object can store things just like a Bundle can, as key-value pairs. It also has the requirement that the values have to be of types supported by a Bundle: primitives, Parcelables, and the like.

To save something in the handle, you can use the set method, and to read a value, get:

viewModel.handle.set("name", name)
viewModel.handle.get<String>("name")

And… For a basic setup, that’s really all there is to it. These steps on their own are enough to have this value persisted in savedInstanceState, and for it to survive process death!

Let’s put this in an example app to see this all working together:

class MyViewModel(val handle: SavedStateHandle) : ViewModel()

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val factory = SavedStateVMFactory(this)
        viewModel = ViewModelProviders.of(this, factory)
                        .get(MyViewModel::class.java)

        saveButton.setOnClickListener {
            val name = nameInput.text.toString()
            viewModel.handle.set("name", name)
            nameText.text = name
            nameInput.setText("")
        }

        // read the current value at the start, in case a value 
        // was restored from savedInstanceState
        nameText.text = viewModel.handle.get<String>("name")
    }
}

This app lets us put in a name and save it with a button click, at which point it’s both stored in the handle and shown in a TextView.

The application with a name saved

We can test if our data really survives process death by using the tried and true method of running the application, putting in the background using the Home button, and killing it via the Logcat panel’s Terminate Application button. Et voilà, reopening the app from the launcher will show the saved name again!

Separating concerns

This direct access to the handle from the Activity isn’t very pretty, so we might want to do something nicer instead, like hide it behind a property with a custom getter and setter:

class MyViewModel(private val handle: SavedStateHandle) : ViewModel() {
    var name: String?
        get() = handle.get<String>("name")
        set(value) {
            handle.set("name", value)
        }
}

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /* Factory call to set up ViewModel... */

        saveButton.setOnClickListener {
            val name = nameInput.text.toString()
            viewModel.name = name
            nameText.text = name
            nameInput.setText("")
        }

        nameText.text = viewModel.name
    }
}

This pattern of a getter and setter would be repeated for every piece of data that we save this way - this is just screaming for a custom property delegate. We can also use the get and set methods with Kotlin’s operator syntax to simplify things. This gets us this implementation:

class HandleDelegate<T>(
    private val handle: SavedStateHandle,
    private val key: String
) : ReadWriteProperty<Any, T?> {
    override fun getValue(thisRef: Any, property: KProperty<*>): T? {
        return handle[key]
    }
    override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
        handle[key] = value
    }
}

class MyViewModel(handle: SavedStateHandle) : ViewModel() {
    var name: String? by HandleDelegate(handle, "name")
}

LiveData

Since this is a library deeply tied to Android Architecture Components, it also comes with LiveData support. The simplest way to make use of this is by - for now - continuing to use the set method, but instead of updating our TextView in our click listener, calling getLiveData just once, and attaching an Observer to the LiveData it returns.

class MyViewModel(val handle: SavedStateHandle) : ViewModel()

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /* Factory call... */

        saveButton.setOnClickListener {
            val name = nameInput.text.toString()
            viewModel.handle.set("name", name) // Saving a value here
            nameInput.setText("")
        }

        viewModel.handle.getLiveData<String>("name")
            .observe(this, Observer { name ->
                nameText.text = name
            })
    }
}

This observer will receive an update every time the Save button is pressed, and it will also be triggered with the already existing value from savedInstanceState when the app was restarted after a process death.

Refactoring again

We’re accessing the handler directly from our Activity yet again. This could be cleaned up, for example, by using the often recommended pattern of storing a private MutableLiveData instance in our ViewModel, and exposing the same instance to our Activity as a read-only LiveData. We can do this because getLiveData actually returns a MutableLiveData subclass - convenient!

class MyViewModel(private val handle: SavedStateHandle) : ViewModel() {
    private val _name: MutableLiveData<String> = 
        handle.getLiveData<String>("name")

    val name: LiveData<String> = _name

    fun setName(name: String) {
        if (name.isNotBlank()) {
            _name.value = name
        }
    }
}

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /* Factory call, button listener... */

        viewModel.name.observe(this, Observer { name ->
            nameText.text = name
        })
    }
}

Why not just expose the MutableLiveData directly? There are certainly cases where you can get away with that. In the code above you can see that the ViewModel can, for example, perform validation on the value you’re trying to save.

What’s the catch?

The only interesting detail I found in the implementation so far is that if you don’t provide a key to the get method when you’re asking the ViewModelProvider for a ViewModel instance, your saved data will be associated with the classname of the ViewModel internally (otherwise, the key is used). This means that reusing the same type of ViewModel class for multiple screens will cause their saved states to overwrite each other by default.

Edit: As Ian Lake pointed out on Twitter, this won’t actually happen, because each ViewModel stores its saved state in its owner’s saved state Bundle, which would be separate for two screens. No catch for now then!

Another edit: I’ve now written a follow-up article that explains where state is stored in more detail, check it out here!

Ok, ok. What’s next?

That’s the main feature set for now! You can find all the demo code shown in this repository.

This library is very much still in alpha, as evidenced by some of the comments in the implementation, but it seems promising - at least if we manage to find the correct abstraction to build on top of it to hide its details.

Confessions

I stumbled upon this Google code lab while trying to wrap up this article with some final links to the classes mentioned. Yes, I know it happens to have the exact same example in it for this part. Great minds think alike. So sue me. So it goes.



You might also like...

Wrap-up 2021

Another year over, a new one's almost begun. Here's a brief summary of what I've done in this one.

Retrofit meets coroutines

Retrofit's coroutine support has been a long time coming, and it's finally coming to completion. Take a look at how you can use it to neatly integrate networking into an application built with coroutines.

The conflation problem of testing StateFlows

StateFlow behaves as a state holder and a Flow of values at the same time. Due to conflation, a collector of a StateFlow might not receive all values that it holds over time. This article covers what that means for your tests.

A Deep Dive into Extensible State Saving

A perhaps overly thorough look at how extensible state saving is implemented in the AndroidX libraries, and how ViewModel state saving specifically is hooked into this system.