State hoisting is an important concept useful to make a Composable stateless (and so easily reusable). The official Compose documentationexplains all the details about this concept.
Let’s use this concept to implement a simple Calculator using Compose:
| @Composable | |
| fun Calculator(state: CalculatorState) { | |
| Column { | |
| TextField(value = state.v1, onValueChange = { state.v1 = it }) | |
| TextField(value = state.v2, onValueChange = { state.v2 = it }) | |
| Text(text = state.result) | |
| } | |
| } |
This code contains just two TextFields to insert two numbers and a Textthat displays the sum of them or an error message if something wrong happens. No modifiers and not a fancy layout, let’s keep it simple to focus on the state management.
The CalculatorState can be defined as a class that contains two MutableStates and calculates the result using a standard Kotlin property that invokes a sum method:
| class CalculatorState { | |
| var v1 by mutableStateOf("0") | |
| var v2 by mutableStateOf("0") | |
| val result get() = sum(v1, v2) | |
| } | |
| fun sum(v1: String, v2: String) = try { | |
| (v1.toInt() + v2.toInt()).toString() | |
| } catch (e: NumberFormatException) { | |
| "Parsing error :(" | |
| } |
Then another composable that creates and remembers the state is necessary, something like this one:
| @Composable | |
| fun CalculatorScreen { | |
| val state = remember { | |
| CalculatorState() | |
| } | |
| Calculator(state = state) | |
| } |
Everything works as expected, when the user enters a text a MutableState is updated, then a recomposition is triggered and the Text is updated based on the value calculated in the result property.
But what if sum is a suspend method? A regular Kotlin property can’t be used anymore, however there is something else that can be useful.
Listening to MutableState changes using snapshotFlow
A MutableState is a really interesting concept in Compose, way more powerful than what appears at a first glance (this article contains some really interesting examples about it and all the Snapshot system).
A property, created using the delegation to a MutableState, manages a single value but allows also to add a listener to be notified when the value change. The snapshotFlow method can be used to read the value from one or more States and been notified every time there is a change:
| class CalculatorState { | |
| var v1 by mutableStateOf("0") | |
| var v2 by mutableStateOf("0") | |
| val result = snapshotFlow { v1 to v2 }.mapLatest { | |
| sum(it.first, it.second) //sum is a suspend method here | |
| } | |
| } |
In this example a Pair with the values v1 and v2 is created, the Flowreturned by snapshotFlow emits immediately the two values and will emit a new Pair every time one of the two values changes. Then using mapLatestthe sum is calculated every time paying attention to delete an ongoing calculation when a new value is available (in a real example a debounce can be added to manage quick inputs without starting too many calculations).
A composable under the hood does something similar to update the UI when something changes, everything works thanks to the Snapshot system. However, as this example shows, the Snapshot system can be used also in a regular class outside a Composable annotated method.
The result property is now a Flow<String> and not just a String, so collectAsState must be used inside the composable to obtain the value (and listen to updates):
| @Composable | |
| fun Calculator(state: CalculatorState) { | |
| Column { | |
| TextField(value = state.v1, onValueChange = { state.v1 = it }) | |
| TextField(value = state.v2, onValueChange = { state.v2 = it }) | |
| Text(text = state.result.collectAsState(initial = "").value) | |
| } | |
| } |
It works but what happens on a configuration change (for example changing the device orientation)? Unfortunately all the data inserted by the user and an eventual ongoing calculation are lost. Using rememberSaveableinstead of remember in CalulatorScreen helps maintaining the values but it doesn’t keep an ongoing calculation alive. The collectAsState method under the hood uses the CoroutineScope associated with the composable. As we know the Activity is destroyed and recreated on a configuration change, so the composable’s CoroutineScope is not maintained (and an ongoing calculation created using it is canceled).
The usual solution to these kinds of problems is to use a ViewModel. Maybe in the future in a “full” Compose app all the orientation changes will be managed as States, however right now using a ViewModel continue to be a good solution.
ViewModel (and viewModelScope) to the rescue
Many Compose examples that use a ViewModel expose the values using LiveDatas or MutableStateFlows. Let’s see how to create something similar to the previous example using a ViewModel that uses MutableStateFlows.
The CalculatorState defined in the previous example can be converted easily to a ViewModel:
| class CalculatorViewModel : ViewModel() { | |
| var v1 = MutableStateFlow("0") | |
| var v2 = MutableStateFlow("0") | |
| val result = v1.combine(v2) { a, b -> a to b}.mapLatest { | |
| sum(it.first, it.second) | |
| }.stateIn(viewModelScope, SharingStarted.Lazily, "0") | |
| } |
Here v1 and v2 are defined as MutableStateFlow instead of MustableState, using combine a Flow that emits a new Pair every time one of the values changes can be created. The stateIn is the key method used in this example, it creates a StateFlow starting from a regular Flow and connects it to the viewModelScope. In this way a sum execution follows the ViewModel’s lifecycle and continues to run even when there is a configuration change.
The CalculatorScreen must be changed to use viewModel instead of remember:
| @Composable | |
| fun CalculatorScreen() { | |
| val viewModel = viewModel<CalculatorViewModel>() | |
| Calculator(state = viewModel) | |
| } |
The composable code is a bit more complicated, collectAsState and value must be used because the properties are MutableStateFlows instead of regular String created using a MutableState:
| @Composable | |
| fun Calculator(state: CalculatorViewModel) { | |
| Column { | |
| TextField(value = state.v1.collectAsState().value, onValueChange = { state.v1.value = it }) | |
| TextField(value = state.v2.collectAsState().value, onValueChange = { state.v2.value = it }) | |
| Text(text = state.result.collectAsState().value) | |
| } | |
| } |
It works as expected also on a configuration change! But the code contains a bit of boilerplate, can it be simplified?
MutableStates in a ViewModel
As discussed before, many Compose examples use MustableStateFlows in the ViewModel. It works and can be useful in case the ViewModel is used also from a standard View based UI. But what about a ViewModel used only by composables? Can it use MutableStates to keep the code simple? This codelab uses this strategy, let’s try to use it in this example as well.
CalculatorViewModel can be changed to use MutableStates and snapshotFlow instead of MutableStateFlows and combine:
| class CalculatorViewModel : ViewModel() { | |
| var v1 by mutableStateOf("0") | |
| var v2 by mutableStateOf("0") | |
| val result = snapshotFlow { v1 to v2 }.mapLatest { | |
| sum(it.first, it.second) | |
| }.stateIn(viewModelScope, SharingStarted.Lazily, "0") | |
| } |
v1 and v2 are now regular Strings, the composable can be simplified to avoid using collectAsState and value:
| @Composable | |
| fun Calculator(state: CalculatorViewModel) { | |
| Column { | |
| TextField(value = state.v1, onValueChange = { state.v1 = it }) | |
| TextField(value = state.v2, onValueChange = { state.v2 = it }) | |
| Text(text = state.result.collectAsState().value) | |
| } | |
| } |
There is a collectAsState invocation because result is still a MutableStateFlow, it can be replaced by a MutableState moving the snapshotFlow invocation to the init block:
| class CalculatorViewModel : ViewModel() { | |
| var v1 by mutableStateOf("0") | |
| var v2 by mutableStateOf("0") | |
| var result by mutableStateOf("0") | |
| private set | |
| init { | |
| snapshotFlow { v1 to v2 } | |
| .mapLatest { | |
| sum(it.first, it.second) | |
| } | |
| .onEach { | |
| result = it | |
| } | |
| .launchIn(viewModelScope) | |
| } | |
| } |
Another advantage of using MutableStates is that exposing a read-only value and make it modifiable only inside the class is really easy. Just a private set is needed instead of defining two properties (one with an _prefix) as it must be done using MutableStateFlows or LiveDatas.
And now the composable can be changed removing also the last collectAsState:
| @Composable | |
| fun Calculator(state: CalculatorViewModel) { | |
| Column { | |
| TextField(value = state.v1, onValueChange = { state.v1 = it }) | |
| TextField(value = state.v2, onValueChange = { state.v2 = it }) | |
| Text(text = state.result) | |
| } | |
| } |
The code is easy to read and the composable contains only the UI, it’s not aware of anything that happens to calculate the values. An interface implemented by the ViewModel can be added to make the Composable even more reusable.
Don’t kill the process!
Another edge case that should be managed by Android apps is saving and restoring the state when the process is killed.
A ViewModel helps to manage a configuration change, however when the process is killed the ViewModelis destroyed too. In the example used in this post if the user enters two values in the UI, then puts the application in background and reopen it later, in case the operating system decides to kill the app, the UI will not contain the original values.
There is a standard way to save data in a ViewModel, a SavedStateHandlecan be used. Using it is not always easy, a delegate can be created to manage a MutableState that uses a SavedStateHandle to read the initial value and save the value on each change:
| class SavedStateHandleDelegate<T>( | |
| private val savedStateHandle: SavedStateHandle, | |
| private val key: String, | |
| defaultValue: T, | |
| ) : ReadWriteProperty<Any, T> { | |
| private val state: MutableState<T> | |
| init { | |
| val savedValue = savedStateHandle.get<T>(key) | |
| state = mutableStateOf( | |
| savedValue ?: defaultValue | |
| ) | |
| } | |
| override fun getValue(thisRef: Any, property: KProperty<*>) = state.value | |
| override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { | |
| state.value = value | |
| savedStateHandle.set(key, value) | |
| } | |
| } |
A SavedStateHandle extension method can be created to simplify the usage:
| fun <T> SavedStateHandle.mutableStateOf( | |
| defaultValue: T, | |
| ) = PropertyDelegateProvider<Any, SavedStateHandleDelegate<T>> { _, property -> | |
| SavedStateHandleDelegate( | |
| savedStateHandle = this, | |
| key = property.name, | |
| defaultValue = defaultValue, | |
| ) | |
| } |
Job Offers
The code to implement the delegate is a bit complicated, however the usage is really simple. The only change is thatMutableStates must be created using the extension method on SavedStateHandle:
| class CalculatorViewModel( | |
| state: SavedStateHandle, | |
| ) : ViewModel() { | |
| var v1 by state.mutableStateOf("0") | |
| var v2 by state.mutableStateOf("0") | |
| var result by state.mutableStateOf("0") | |
| private set | |
| init { | |
| snapshotFlow { v1 to v2 } | |
| .mapLatest { | |
| sum(it.first, it.second) | |
| } | |
| .onEach { | |
| result = it | |
| } | |
| .launchIn(viewModelScope) | |
| } | |
| } |
It works, every time there is a change the delegate saves the value to the SavedStateHandle. When the ViewModel is created the value from the SavedStateHandle (if available) is used. There is just a small bug when the process is killed and restarted: the sum method is invoked even if the result value was available in the SavedStateHandle. The reason is that the init method is always invoked and snapshotFlow emits a Pair with the values read from the SavedStateHandle.
To fix this bug an extra parameter with a lambda used to calculate the value can be added. This lambda contains two parameters: the value read from the SavedStateHandle and another lambda to set the value. Here’s the code:
| class SavedStateHandleDelegate<T>( | |
| private val savedStateHandle: SavedStateHandle, | |
| private val key: String, | |
| defaultValue: T, | |
| initializer: (valueLoadedFromState: T?, setter: (T) -> Unit) -> Unit | |
| ) : ReadWriteProperty<Any, T> { | |
| private val state: MutableState<T> | |
| init { | |
| val savedValue = savedStateHandle.get<T>(key) | |
| state = mutableStateOf( | |
| savedValue ?: defaultValue | |
| ) | |
| initializer(savedValue, ::updateValue) | |
| } | |
| override fun getValue(thisRef: Any, property: KProperty<*>) = state.value | |
| override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { | |
| updateValue(value) | |
| } | |
| private fun updateValue(value: T) { | |
| state.value = value | |
| savedStateHandle.set(key, value) | |
| } | |
| } |
And here’s the extension method code with the extra parameter:
| fun <T> SavedStateHandle.mutableStateOf( | |
| defaultValue: T, | |
| initializer: (valueLoadedFromState: T?, setter: (T) -> Unit) -> Unit = { _, _ -> }, | |
| ) = PropertyDelegateProvider<Any, SavedStateHandleDelegate<T>> { _, property -> | |
| SavedStateHandleDelegate( | |
| savedStateHandle = this, | |
| key = property.name, | |
| defaultValue = defaultValue, | |
| initializer = initializer | |
| ) | |
| } |
In the ViewModel the code that was in the init block can be moved to the new lambda:
| class CalculatorViewModel( | |
| state: SavedStateHandle, | |
| ) : ViewModel() { | |
| var v1 by state.mutableStateOf("0") | |
| var v2 by state.mutableStateOf("0") | |
| val result by state.mutableStateOf("0") { valueLoadedFromState, setter -> | |
| snapshotFlow { v1 to v2 } | |
| .drop(if (valueLoadedFromState != null) 1 else 0) | |
| .mapLatest { | |
| sum(it.first, it.second) | |
| } | |
| .onEach { | |
| setter(it) | |
| } | |
| .launchIn(viewModelScope) | |
| } | |
| } |
A drop invocation can be added to ignore the first value (and avoid executing the calculation) in case a value was read from the SavedStateHandle. Another small improvement is that now result can be defined as a val, it’s changed only inside the new lambda using the setterparameter.
There is still an issue on another edge case if the process is killed while the sum calculation is in progress. It can be fixed by managing the loading state explicitly (right now there is no feedback to the users when a calculation is in progress to keep the example simple).
How to test it
A benefit of moving the logic outside composables is that it can be tested easily using a JVM test. First of all we need a JUnit rule to replace the Maincoroutine dispatcher in the test, here you can find an implementation of it. Then we can write a test that includes this rule, creates the ViewModel and checks the result value after changing the two inputs:
| class ViewModelTest { | |
| @get:Rule | |
| val mainCoroutineRule = MainCoroutineRule() | |
| private val viewModel by lazy { | |
| CalculatorViewModel(SavedStateHandle(mutableMapOf())) | |
| } | |
| @Test | |
| fun `sets the result when an input changes`() { | |
| viewModel.v1 = "10" | |
| viewModel.v2 = "20" | |
| assertThat(viewModel.result).isEqualTo("30") | |
| } | |
| } |
A really simple test with just a small problem: it fails! The problem is that snapshotFlow relies on snapshots, a new value is emitted when a new snapshot is applied and not just when a value changes. In the production code Compose manages all the snapshots under the hood and so everything works. In the test we need to take care of the snapshot, it can be done by wrapping the two lines of code that modifies the values with a Snapshot.withMutableSnapshot invocation:
| @Test | |
| fun `sets the result when an input changes`() { | |
| Snapshot.withMutableSnapshot { | |
| viewModel.v1 = "10" | |
| viewModel.v2 = "20" | |
| } | |
| assertThat(viewModel.result).isEqualTo("30") | |
| } |
Wrapping up
MutableStates and MutableStateFlows are, for some usages, quite similar and can be used to solve the same range of problems. This post contains an example on how to use these two classes, it doesn’t pretend to be the definitive guide on how to use them.
MutableStateFlows are already used in many applications, sometimes (for example when they are used only in composables) they can be replaced with MutableStates to simplify the code. Other times a MutableStateFlow is a better choice, for example when one of the many operators available is necessary. However using the snapshotFlow method a MutableState can be converted to a Flow, a good tradeoff can be to use MutableStates and use this method to switch to Flows if necessary.
The examples in this post don’t involve using a “real” hot flow (like a flow that emits the device position using the GPS), in that case a Flow is the best solution to remove the subscription when the application is in background.
This repo contains the code of the final example (with the test) described in this post.
Compose is still quite new and so there aren’t years of best practices to follow. A ViewModel seems to be a best practice of the pre-Compose era but, as explained in this post, can be useful using Composables as well.



