Blog Infos
Author
Published
Topics
Published

Don’t do this State Mistake in Jetpack/Jetbrain Compose — State Destructuring vs State Delegates.

There are two ways to create a Mutable State in Jetpack/Jetbrain Compose. The first and my favorite way is by using Destructing and another way is to use Delegate.

Modified Photo by Anders Norrback Bornholm on Unsplash

Mutable State with Destructing 🔨

If you don’t know the concept of Destructing in Kotlin Learn all about them from here :

If you look at the MutableState signature :

@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
view raw MutableState.kt hosted with ❤ by GitHub

It has component1 and component2 operators which means, it can be destructed like :

val(count, setCount) = mutableStateOf(0)

Where count gets its value from component1 and setCount is a lambda coming from component2 .

Mutable State with Delegates 🍇

Learn delegate from the official channel :

https://kotlinlang.org/docs/delegated-properties.html

If you look into State and Mutable State documentation :

inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}
view raw delegate.kt hosted with ❤ by GitHub

getValue(...) is defined over State and setValue(...) is defined over MutableState, thus we can use the concept of delegate over it example :

// dont forget these imports
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
// for read only from state
val count by mutableStateOf(0)
// for read and write on state
var count by mutableStateOf(0)
view raw readwrite.kt hosted with ❤ by GitHub

We control reading and writing on a state by defining val or var reference type of variable.

In my personal opinion using Destructing way feels more readable and maintainable to me, as I’m aware of where the set operation has happened by tracking setCount(), and where the get operation is happening by tracking count. Whereas tracking state created from delegate you will have a hard time, also In my experience, I feel working with var ends up being exploited by developers easily.

Also in scenarios where I’m forwarding state to children composable, I don’t have to create in-place lambdas I can directly pass the reference, for instance :

val(isChecked, setChecked) = remember {
mutableStateOf(value = false)
}
Checkbox(
checked = isChecked,
onCheckedChange = setChecked
)
// vs
var checked by remember {
mutableStateOf(value = false)
}
Checkbox(
checked = checked,
onCheckedChange = /* in place lambda */{
checked = it
}
)
view raw stateVs.kt hosted with ❤ by GitHub

Well until now I have been a fan of destructing mutable states, but recently I have encountered a scenario which raised concern in my mind regarding this approach.

Consider the following code, I’m incrementing state by 1 when a button is clicked and displaying it on UI :

// Create a counter state
val(count, setCount) = remember {
mutableStateOf(0)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// display state on UI
Text(
text = "$count", //👈 display current value of state
fontSize = 32.sp,
)
// click button to increment count by 1
Button(
onClick = {
setCount(count + 1) //👈 update state
}
) {
Text(text = "Increment by 1")
} // button end
} // column end

Output :

 

Button click increment state by 1 using state destructuring

 

I want to update the Snippet to increment by 2 instead of 1, see the following changes I have done :

// Create a counter state
val(count, setCount) = remember {
mutableStateOf(0)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// display state on UI
Text(
text = "$count", //👈 display current value of state
fontSize = 32. sp,
)
// click button to increment count by 2
Button(
onClick = {
setCount(count + 1) //👈 update state 1st time
setCount(count + 1) //👈 update state 2nd time
}
) {
Text(text = "Increment by 2")
} // button end
} // column end
view raw destructure2.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Build adaptive apps with Jetpack Compose

In this workshop you will learn how to build adaptive apps for phones, tablets, and foldables, and how they enhance reachability with Jetpack Compose.
Watch Video

Build adaptive apps with Jetpack Compose

Miguel Montemayor
Developer Relations Engineer
Google

Build adaptive apps with Jetpack Compose

Miguel Montemayor
Developer Relations ...
Google

Build adaptive apps with Jetpack Compose

Miguel Montemayo ...
Developer Relations Engin ...
Google

Jobs

Output :

 

Button click increment state by 2 using state destructuring

 

🤯 WTF — it didn’t work!

Let’s Investigate 🤔 why?

when you click on the button following is what is happening in the background :

Button(
onClick = {
// thing that is happening is :
setCount(count + 1) //👉 0 + 1
setCount(count + 1) //👉 0 + 1
}
) {
Text(text = "Increment by 2")
} // button end
view raw onClick.kt hosted with ❤ by GitHub

but why??? setCount() should be up the mutable state right??

What broke?? is it Compose or MutableState?? :

Well, none is broken. I dig a bit deep into what happened and I figured out that :

  • Compose internally observe all reads and writes to state, we don’t need to understand inner working for this discussion, but you just need to know that compose works on a snapshot system, all the writes you do on mutable State effects this snapshot.
  • If there is a conflict between the previous and new snapshot then some action will happen to resolve the conflict. In terms of Compose Framework, it would lead to a trigger to re-composition.
  • So when the mutable state value is updated it would check the snapshot policy and validate if snapshot is having any conflict,
  • if the answer to that is yes, then the mutable state would inform compose to recompose and store the latest value, but instead of making the latest value an immediate state value, it would set it as a candidate value for the next recomposition cycle to read from.
  • Thus even if you have done multiple state updates, the last value you set would be treated as the latest candidate for the state value.
  • When recomposition would happen it reads the latest value from the mutable state and apply changes.

🤷‍♂️Okay… does that mean we have to wait until candidate value becomes real value i.e. till recomposition to update the state? i.e. doing multiple set State operations in a row is wrong in compose???

Well In most cases we can avoid this situation if we create a launchEffect and put our state change logic into that or refactor our logic to create only one set state.

but ideally, this isn’t a behavior we expect from updating a state.

Let’s see the same code using the delegate pattern :

// Create a counter state
var count by remember {
mutableStateOf(value = 0)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// display state on UI
Text(
text = "$count", //👈 display current value of state
fontSize = 32. sp,
)
// click button to increment count by 2
Button(onClick = {
count = count + 1 // update state
count = count + 1 // update state
}) {
Text(text = "Increment by 2")
}// button end
} // column end

Output :

 

Button click increment state by 2 using state delegation

 

this snippet would perform the operation correctly! 🤯. Why did this happen? doesn’t the same rule be applied to the behavior?

Well no! Creating State with Destructing ≠ Creating Sate with Delegate

The internally same thing has happened, but when you use state delegation method, whenever you are reading a value from the state it is delegating the operation of getting to MutableState.value, which returns candidate value instead of real value (if snapshot ids have changed).

But when you are working with Destructing state, component1 has only called MutableState.value only once while destructing and has set it to count variable i.e getter part. if you again read it from the getter it won’t delegate the task to MutableState.value check and get the candidate value it would just return back last computed value when it was de-structuring.

that means while using Destructuring you have written code similar to :

@Composable
fun Foo() {
val countState = remember { mutableStateOf(0) }
Button(onClick = {
val count = countState.value //👈 count is 0
val setCount = { lastestCount: Int ->
countState.value = lastestCount
}
setCount(count+1) //👈 0 + 1
setCount(count+1) //👈 0 + 1
}) {
Text(text = "Increment by 2")
}// button end
}

Thus you have to wait for recomposition to re-initialize the value of the mutable state.

So actually no behavior of Compose or MutableState is broken, it is just the way destructuring and delegation work in the language.

Conclusion 💆🏻‍♀️

Though I still feel Destructing is better than Delegating state but to work with the consistent state value it would be best to go with the Delegation path.

Thanks for continuing some far. If you liked my article check out more of my article and content from https://chetangupta.net/

If you want me to write an article or train Android teams for your business do reach out by clicking 👉 here.

Until Next Time! Happy Hacking 👨🏻‍💻

This article was originally published on proandroiddev.com on October 18, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu