

An Early Look at ViewModel SavedState
source link: https://www.tuicool.com/articles/hit/fm6r2if
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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, 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.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK