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 factorySavedStateVMFactory
and your ViewModel should have a constructor that receivesSavedStateHandle
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, Parcelable
s, 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
.
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...
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.
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.