Blog Infos
Author
Published
Topics
, , ,
Published

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 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)
}
}
view raw Calculator.kt hosted with ❤ by GitHub

This code contains just two s to insert two numbers and a 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 can be defined as a class that contains two s and calculates the using a standard Kotlin property that invokes a 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 is updated, then a recomposition is triggered and the is updated based on the value calculated in the property.

But what if is a method? A regular Kotlin property can’t be used anymore, however there is something else that can be useful.

Listening to MutableState changes using

A 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 , 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 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 with the values and is created, the returned by emits immediately the two values and will emit a new 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 annotated method.

The property is now a and not just a , so 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)
}
}
view raw Calculator.kt hosted with ❤ by GitHub

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 instead of in helps maintaining the values but it doesn’t keep an ongoing calculation alive. The method under the hood uses the associated with the composable. As we know the is destroyed and recreated on a configuration change, so the composable’s is not maintained (and an ongoing calculation created using it is canceled).

The usual solution to these kinds of problems is to use a . Maybe in the future in a “full” Compose app all the orientation changes will be managed as s, however right now using a continue to be a good solution.

ViewModel (and viewModelScope) to the rescue

Many Compose examples that use a expose the values using s or s. Let’s see how to create something similar to the previous example using a that uses s.

The defined in the previous example can be converted easily to a :

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 and are defined as instead of , using a that emits a new every time one of the values changes can be created. The is the key method used in this example, it creates a starting from a regular and connects it to the . In this way a execution follows the ViewModel’s lifecycle and continues to run even when there is a configuration change.

The must be changed to use instead of :

@Composable
fun CalculatorScreen() {
val viewModel = viewModel<CalculatorViewModel>()
Calculator(state = viewModel)
}

The composable code is a bit more complicated, and must be used because the properties are s instead of regular created using a :

@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)
}
}
view raw Calculator.kt hosted with ❤ by GitHub

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 s in the . It works and can be useful in case the is used also from a standard View based UI. But what about a used only by composables? Can it use s to keep the code simple? This codelab uses this strategy, let’s try to use it in this example as well.

can be changed to use s and instead of s and :

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")
}

and are now regular s, the composable can be simplified to avoid using and :

@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)
}
}
view raw Calculator.kt hosted with ❤ by GitHub

There is a invocation because is still a , it can be replaced by a moving the invocation to the 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 s is that exposing a read-only value and make it modifiable only inside the class is really easy. Just a is needed instead of defining two properties (one with an prefix) as it must be done using s or s.

And now the composable can be changed removing also the last :

@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)
}
}
view raw Calculator.kt hosted with ❤ by GitHub

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 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 helps to manage a configuration change, however when the process is killed the 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 , a SavedStateHandlecan be used. Using it is not always easy, a delegate can be created to manage a that uses a 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 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Testing: how hard can it be?

When people start looking into the testing domain, very similar questions arise: What to test? And more important, what not? What should I mock? What should I test with unit tests and what with Instrumentation?…
Watch Video

Testing: how hard can it be?

DANNY PREUSSLER
Android Lead
Soundcloud

Testing: how hard can it be?

DANNY PREUSSLER
Android Lead
Soundcloud

Testing: how hard can it be?

DANNY PREUSSLER
Android Lead
Soundcloud

Jobs

The code to implement the delegate is a bit complicated, however the usage is really simple. The only change is thats must be created using the extension method on :

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 . When the is created the value from the (if available) is used. There is just a small bug when the process is killed and restarted: the method is invoked even if the value was available in the . The reason is that the method is always invoked and emits a with the values read from the .

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 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 the code that was in the 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 invocation can be added to ignore the first value (and avoid executing the calculation) in case a value was read from the . Another small improvement is that now can be defined as a , it’s changed only inside the new lambda using the 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 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 and checks the 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 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 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

s and 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.

s are already used in many applications, sometimes (for example when they are used only in composables) they can be replaced with s to simplify the code. Other times a is a better choice, for example when one of the many operators available is necessary. However using the method a can be converted to a , a good tradeoff can be to use s and use this method to switch to 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 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 seems to be a best practice of the pre-Compose era but, as explained in this post, can be useful using Composables as well.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog
Compose is part of the Jetpack Library released by Android last spring. Create Android…
READ MORE

1 Comment. Leave new

  • Noah Ortega
    04.10.2022 20:54

    Great article! Really helped me with my project.
    I do have one question though.

    For what reason did you change this line
    > var v1 by mutableStateOf(“0”)
    to this
    > var v1 = MutableStateFlow(“0”)
    when changing the CalculatorState class into a viewmodel?
    Is there a problem with using the delegate method?

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu