Dealing with stateless, immutable objects is easy and predictable, but real life is complicated. User interface has its state, and sometimes it's our responsibility to manage it.
Problem
Let's take a look at a simple application where you can increment a number in a text field:
<RelativeLayout …>
<TextView
android:id="@+id/textView"
…
android:text="0" />
<EditText
android:id="@+id/editText"
…
android:text="0" />
<Button
android:id="@+id/button"
…
android:text="+" />
</RelativeLayout>class CounterActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val tv = findViewById<TextView>(R.id.textView)
val et = findViewById<EditText>(R.id.editText)
findViewById<Button>(R.id.button).setOnClickListener {
tv.text = ((tv.text.toString().toIntOrNull() ?: 0) + 1).toString()
et.setText(((et.text.toString().toIntOrNull() ?: 0) + 1).toString())
}
}We had a TextView and an EditText handled by identical code invoked from a Button. But when the screen was rotated, our activity was recreated, layout was re-inflated, and we lost TextView.text but EditText.text had survived.
There are two reasons for Android to recreate our Activity. The first one is a configuration change (device orientation, language, font scale, and much more). Some of them could be handled by your application and recreation might be avoided. But the second reason is process death: if some other foreground application uses a lot of memory, your application in background will be killed. We can't avoid it, so we have to save our state. (People telling that applications are killed very rarely are probably iPhone users.)
View state
Generally, a View with a specified ID will preserve its state automatically. The key in the example above is that EditText is a widget for user input and is designed to save the text, while TextView is intended for output and doesn't want to save its text by default. You can assign android:freezesText="true" to change this behavior but that's not a clean solution: our logic relies on data stuck inside a view, including that intended for output only.
Several Views having the same ID would lead to state loss and even crashes. View hierarchy state is just a Map<ID, state> so the last saved View evicts the preceding View's state and imposes other views to use it for restoring. Also, list widgets like ListView, GridView, RecyclerView never save the state of their contents — it's the responsibility of Adapter that owns the data.
Saved instance state
There's the onSaveInstanceState(Bundle) method available for override both in Fragment and Activity. It gives us a chance to save our data in both scenarios: configuration change and process death. But keep in mind that if the user exits via the "back" button, the state is not to be saved, and new opening of this screen will be "clean".
Bundle is just a Map with String keys capable of storing primitive, String, Serializable, Parcelable values, as well as collections of those. The restriction is caused by the fact that the saved state will leave our app as a stream of bytes in case of our process death, and back into new process if our app is resurrected. That means we can't store arbitrary objects there.
Here's an example with two important changes: it never loses the state, and never gets data from views. Now the Activity owns our data and is responsible for preserving it.
class CounterActivity : Activity() {
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val tv = findViewById<TextView>(R.id.textView)
val et = findViewById<EditText>(R.id.editText)
if (savedInstanceState != null) {
// recreation, use saved state
count = savedInstanceState.getInt("count")
}
tv.text = count.toString()
et.setText(count.toString())
findViewById<Button>(R.id.button).setOnClickListener {
count++
tv.text = count.toString()
et.setText(count.toString())
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// we're being destroyed, save the data for the following recreation
outState.putInt("count", count)
}
}ViewModel
As noted before, not everything can be persisted in a Bundle. Also, interprocess communication transactions have the size limit of 1MB (which may actually be smaller on old or low-end devices). Of course, user input have to be preserved carefully because it's extremely frustrating for the user to waste some time on filling in some data which will be lost. But what about some non-critical data?
Something that was downloaded from the internet can just be downloaded once more or simply taken from local cache file.
Data from a local database can easily be requested one more time.
Internet radio or video stream plays in real time and doesn't need to be stored.
Any other real-time connection like the one for instant messaging can be reestablished later.
In all these scenarios we'd like not to lose the data in case of configuration change but dismiss it in case of process death because the user is currently completely focused on something different and we don't know when they're going to return. Anyway, we can't persist a socket! When our app is killed, all our network connections are gone.
That's when we need a ViewModel. Its name comes from the MVVM (Model-View-ViewModel) architecture pattern where Model is our business layer like data tables, actions, network communications and so on; View is our presentation layer managed by Views visible to the user; and ViewModel is a presentation-layer logic which holds data we're currently showing. Let's imagine such a separation using an example with the Notes application:
Model: database tables
notesandnote_attachments, classesNoteandAttachment, actionscreateNote,editNote,addNoteAttachment.View:
activity_note.xmlused byNoteActivityclass responsible for creating and editing,dialog_attachment.xmlandAttachmentDialogclass responsible for taking photos or recording voice notes.NoteViewModelresponsible for fetching and saving note content.
Such a structure allows Model to be unaware of the presentation layer so that Model+ViewModel can be unit-tested easily, and the View remains so simple that we may even not have to test it at all, especially knowing that UI tests are harder to write, slower to run, and flakier.
Back to Android: ViewModel class from Android Architecture Components is just a thing that won't be abandoned due to configuration change. Its usage scenario looks like this: an Activity or Fragment requests a specific ViewModel letting ViewModelStore either create it from scratch or peek one that already exists. This way, our open network or database connections and fetched data will outlive our Views.
First, we need to add a dependency to our module-level build.gradle file (check out for the latest version):
dependencies {
…
implementation("androidx.activity:activity-ktx:1.6.1")
}Then we can write down a ViewModel for our toy counter:
class CounterViewModel : ViewModel() {
var count = 0
private set
fun increment() {
count++
}
}And finally, switch to using ViewModel in our Activity:
class CounterActivity : ComponentActivity() {
// we need to extend ComponentActivity or AppCompatActivity
// for viewModels() extension to work
private val vm: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val tv = findViewById<TextView>(R.id.textView)
val et = findViewById<EditText>(R.id.editText)
tv.text = vm.count.toString()
et.setText(vm.count.toString())
findViewById<Button>(R.id.button).setOnClickListener {
vm.increment()
tv.text = vm.count.toString()
et.setText(vm.count.toString())
}
}
}You can check this on your own: the counter value survives configuration changes (like device rotation) but will be dropped in case of process death (you can open Camera and web browser apps to drain all RAM) – only the EditText value will be preserved in that case.
Providing data from ViewModel to View
Our current ViewModel has a major pitfall: it cannot push data into the View. Any asynchronous operations should run inside ViewModel to outlive View when it is recreated, but we need a way to deliver the results, and View should observe them. So we need a container which holds a value and gives us an ability to subscribe to changes.
There's the LiveData class in Android Architecture Components exactly for this purpose. But kotlinx.coroutines have their own StateFlow which provides better null-safety and is very handy to use with coroutines so we're going to stick with it.
First, we need to add coroutines library and Android lifecycle integration library to our dependencies:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")Now let's wrap our count value in MutableStateFlow, that is, an observable container which can be read from and written to. Instead of private set, we will expose it to the View as StateFlow, which cannot be written to because it is only ViewModel's responsibility to change the count.
class CounterViewModel : ViewModel() {
- var count = 0
- private set
+ private val _count = MutableStateFlow(0)
+ val count: StateFlow<Int> get() = _count
fun increment() {
- count++
+ _count.value++
}
}Now instead of polling the value from the View we will observe it which means that the View will be notified about any changes in the StateFlow.
class CounterActivity : ComponentActivity() {
private val vm: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- tv.text = vm.count.toString()
- et.setText(vm.count.toString())
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ vm.count.collect { count ->
+ tv.text = count.toString()
+ et.setText(count.toString())
+ }
+ }
+ }
findViewById<Button>(R.id.button).setOnClickListener {
vm.increment()
- tv.text = vm.count.toString()
- et.setText(vm.count.toString())
}
}
}That's a lot, especially to get the same result!
First, we
launcha coroutine in a lifecycle-aware scope. This gives us a chance to usesuspendfunctions inside launch's lambda-parameter. As it is tied to view's lifecycle, any operation running in this scope will be canceled when the view is destroyed. Thus, if the View gets recreated, it will abandon the subscription and avoid possible memory leak caused by long-livingViewModel'sStateFlowholding a reference to short-lived View subscriber.Next,
repeatOnLifecyclecreates a narrower scope between Activity'sonStartandonStoplifecycle methods: anything inside inner lambda will run inonStartand will be canceled whenonStophappens. So, when the user is incapable of seeing the View, the latter never observes the data.Finally,
collectfunction saves our lambda to be called both at the beginning of the suscription (inonStart) and when the data gets changed (inincrementfunction in our example). This makes our View up to date with data source.We don't need the second
.text = …block! Every timeincrementfunction changes theStateFlowvalue our collector lambda is invoked.
As a bonus, we can apply any transformations to the value of Flow, like this:
vm.count.map(Int::toString).collect { count ->
tv.text = count
et.setText(count)
}
If you're familiar with RxJava, you can think of MutableStateFlow as a BehaviorSubject.
ViewModel parameters and state
We've got all set up but there are some blind spots to be explained about ViewModel.
As you may have noticed, we've never created a ViewModel instance explicitly. Instead, we've just said "give us the ViewModel of that class" and it was either instantiated or returned from cache. This means that our ViewModel class should have a public parameterless constructor. But what if we need to pass something? Inside the Notes app, we need to transfer a note ID from Activity.intent or Fragment.arguments in order to fetch one, for example. Of course, we could do this by adding an init method and invoking it from onCreate but that would lead to everything lateinit inside the ViewModel, which is kind of problematic to cope with: it is always a bad idea to reinvent the constructor!
Instead, a Factory can be provided to be invoked when a ViewModel is needed. Factory is designed in such a way that a single factory could instantiate ViewModels of different types so initially this looks wordy.
private val vm: CounterViewModel by viewModels(
factoryProducer = {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =
CounterViewModel(/* pass arguments here */) as T
}
}
)Instead, we'd like to see something simple and type-safe, like the following:
private val vm by viewModel(
factory = { CounterViewModel(/* pass arguments here */) }
)One way to achieve this is to use these extensions:
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
// for Activity
inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factory: (CreationExtras.() -> VM)? = null
): Lazy<VM> =
viewModels(extrasProducer, factory?.asVmFactoryProducer())
// for Fragment
inline fun <reified VM : ViewModel> Fragment.viewModel(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factory: (CreationExtras.() -> VM)? = null
): Lazy<VM> =
viewModels(ownerProducer, extrasProducer, factory?.asVmFactoryProducer())
fun (CreationExtras.() -> ViewModel).asVmFactoryProducer(): (() -> ViewModelProvider.Factory) =
{
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =
this@asVmFactoryProducer(extras) as T
}
}Just copy-paste them to your project and you'll get a proper way of instantiating ViewModels.
Now the last thing remains: ViewModel can have its own state which will be preserved across process recreations. ViewModel can accept SavedStateHandle , which is a wrapper around Bundle.
class CounterViewModel(
+ private val savedStateHandle: SavedStateHandle
) : ViewModel() {
- private val _count = MutableStateFlow(0)
+ private val _count = MutableStateFlow(savedStateHandle.get<Int>("count") ?: 0)
val count: StateFlow<Int> get() = _count
fun increment() {
_count.value++
+ savedStateHandle["count"] = _count.value
}
}This constructor signature is also supported by the default ViewModel Factory. But if you need something apart from SavedStateHandle, you can get one by calling the CreationExtras.createSavedStateHandle() extension function in your own factory.
private val vm by viewModel(
factory = { CounterViewModel(createSavedStateHandle(), /* other arguments */) }
)CreationExtras is a Map which can be used to pass any additional data into ViewModel Factory, especially when one factory is capable of instantiating different ViewModels with common dependencies.
Conclusion
By default, Android preserves user input like EditText.text, CheckBox.isChecked and so on. Any output like TextView.text or Button.isEnabled is your responsibility as the application developer.
Never rely on the state of View: it makes presentation tightly coupled with logic, complicating testing and redesign.
ViewModel is a tool for keeping transient state across configuration changes: it's better to preserve downloaded data instead of downloading it one more time when the user did such a common thing as device rotation.
On the other hand, savedInstanceState and SavedStateHandle are responsible for holding small sets of important data. Never lose user input, the users won't forgive you for this!