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 TextField
s to insert two numbers and a Text
that 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 MutableState
s 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 State
s 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 Flow
returned 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 rememberSaveable
instead 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 State
s, 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 LiveData
s or MutableStateFlow
s. Let’s see how to create something similar to the previous example using a ViewModel
that uses MutableStateFlow
s.
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 MutableStateFlow
s 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 MustableStateFlow
s 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 MutableState
s 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 MutableState
s and snapshotFlow
instead of MutableStateFlow
s 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 String
s, 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 MutableState
s 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 MutableStateFlow
s or LiveData
s.
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 ViewModel
is 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 thatMutableState
s 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 setter
parameter.
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 Main
coroutine 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
MutableState
s and MutableStateFlow
s 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.
MutableStateFlow
s are already used in many applications, sometimes (for example when they are used only in composables) they can be replaced with MutableState
s 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 MutableState
s and use this method to switch to Flow
s 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.