Understanding the Role of State and Implement the Simplest Way of Managing State in Jetpack Compose UIs

Veronica Putri Anggraini
10 min readApr 6, 2023

Halo folks, in this article let’s discuss about state management in jetpack compose. I was share based on my experience and exploration, so feel free to give feedback in the comment section, and discuss together.

When we talking about Jetpack Compose, we will think about declarative UI toolkit with reactive programming approach. It’s make the UI is automatically updated based on changes to the state of the app. If you not familiar with declarative concept you can check this article to have better understanding about declarative UI approach. It will be difficult to keep the UI sync with the app state especially in complex applications with multiple screen and data sources. Because of that, we need state management that enable us defining how the UI should be updated based on change to the app state.

Beside that, using jetpack compose allow us have reactive and dynamic UI that respond to change the app state. Because compose evaluate the function to determine what needs to be updated in the UI, by comparing the previous and current version of the function and looking for differences between the two of them, actually this is a recomposition. But sometimes it’s make a tons of unnecessary recomposition, to reduce the number of unnecessary recompositions, we can use state management. We can carefully managing state and only recomposing the UI when necessary, and of course it will be reduce the workload on the CPU and GPU, improve the performance and ensure that the app remains smooth and responsive.

There are several way to manage state in Jetpack Compose, like these below :

  1. Mutable State, this is the basic way to manage state in Jetpack Compose. We need to create a variable that holds the state and then using the remember function to ensure that state is preserved across recompositions.
  2. State Hoisting, this is lifting state up the hierarchy of the UI tree to common ancestor component so that it can be shared between multiple child components and it will be reduce the excessive state management and help simplify the UI tree.
  3. Derived State, this is computing state from other state value or external data. It can help reduce the amount of state that need to be stored and make the code more modular and easier to maintain.
  4. ViewModel, this is a architecture component that can be used to stored and manage UI-related data. It’s help us to handle complex state or data that needs to be shared between multiple composable.
  5. Live Data and StateFlow, data structures that can be used to emit state changes and trigger recompositions in Jetpack Compose. They are particularly useful for handling asynchronous state changes or updates from external data sources.

Let’s discuss more detail about Mutable State as the simplest technique for managing state in compose. It useful for managing simple state values, such as counters or booleans, that need to be updated in response to user interactions. In another word, it’s not suitable for managing more complex state that needs to be shared between multiple components or persisted across configuration change.

Here the sample of mutable state implementation :

Basically to implement mutable state, we need to know about two type of it.

  1. Destructuring
  2. Mutable Delegate

The sample above is Mutable Delegate structure implementation. It’s allow us to delegate the storage and management of state to another object, such as a ViewModel or another stateful object. And to handle writing and reading on the state is through defining val or var reference type of variable. Like the sample below :

var favorite by remember { mutableStateOf(false) }

Besides Mutable Delegate, we can use Destructuring, it’s allow us to extract multiple values from a single mutable state object using destructuring declarations. When the FavoriteButton function migrated into Destructuring approach, it will be like this code below :

Here the full code. When you rendering the code it will like this.

There is no differences from the compiling result between both of them. But the key difference between mutable destructuring and mutable delegate is that mutable destructuring allows us to extract multiple values from a single mutable state object, while mutable delegate allows us to delegate the storage and management of state to another object. Mutable delegate is particularly useful for managing complex state or state that needs to be shared between multiple components. It’s worth noting that both mutable destructuring and mutable delegate are implemented using Kotlin’s property delegation feature, which allows us to customize the behavior of property access and modification. This makes them powerful and flexible techniques for managing state in Jetpack Compose.

But one thing to keep in mind when using mutable state is that recompositions can be triggered frequently in Jetpack Compose, especially during animations or when scrolling. This can result in frequent updates to the state variable and potentially slow down the app. To avoid this, it’s important to be mindful of the performance implications of using mutable state and use it judiciously.

In this article will show the sample of Mutable State implementation, and for other way to manage state will discuss on the next article. Now, let’s try to create simple app using mutable state :

  • First, create the new project. After create app with compose template you can add some assets on drawable, you can download the assets from here.
  • Besides adding asset, add the string below.
<resources>
...
<string name="sad_quotes">It is amazing how someone can break your heart and you can still love them with all the little pieces.</string>
<string name="pressure_quotes">Pressure is the single mom who is trying to scuffle and pay her rent. We get paid a lot of money to play a game. Don\'t get me wrong: there are challenges. But to call it pressure is almost an insult to regular people.</string>
<string name="bored_quotes">I\'m bored is a useless thing to say. I mean, you live in a great, big, vast world that you’ve seen none percent of. Even the inside of your own mind is endless; it goes on forever, inwardly, do you understand? The fact that you’re alive is amazing, so you don\'t get to say: I\'m bored.</string>
<string name="happy_quotes">Happy people is a great hope to all the unhappy people because a path of success can always be used by the people who couldn\'t find such a path yet!</string>
<string name="laughing_quotes">I love people who make me laugh. I honestly think it\'s the thing I like most, to laugh. It cures a multitude of ills. It\'s probably the most important thing in a person.</string>
<string name="love_quotes">It\'s so easy to love someone at their best; to be happy when everything is going right. The true test is loving them and understanding them at their worst; because that\'s when they need you most. If they can\'t love you through the bad times, they don\'t deserve you through the good ones.</string>
</resources>
  • Then add color below on your project.
val blueDFEDF3 = Color(0XFFDFEDF3)
val blueA9CADA = Color(0XFFA9CADA)
  • Create composable function with name GaleryApp. Use Constraint Layout to make easier to handle widget inside. Change the background of app with add background color like the code below.
@Composable
fun EmoticonApp(modifier: Modifier = Modifier) {

ConstraintLayout(
modifier
.fillMaxSize()
.background(color = blueA9CADA)
) {
...
}
  • Change the statusbar color, open the Theme file and change the color like the code below.
(view.context as Activity).window.statusBarColor = blueA9CADA.toArgb()
  • Create custom Image as button with name ImageButton like the code below. Give several parameter like image with Int data type, it will make easier to call value resource from drawable and make the code more reusable.
@Composable
fun ImageButton(modifier: Modifier = Modifier, image: Int, onClick: () -> Unit) {
Image(
painter = painterResource(image),
modifier = modifier
.size(100.dp)
.padding(10.dp)
.clickable { onClick() },
contentDescription = null
)
}
  • We need to create custom Image for the main emoticon too, create the custom image with name ImageTop and parameter image, like the following code.
@Composable
fun ImageTop(modifier: Modifier = Modifier, image: Int) {
Image(
painter = painterResource(image),
contentDescription = null,
modifier
.padding(top = 20.dp)
)
}
  • After that, we need to create the quote text with text parameter, so it will be reusable for each condition emoticon, like the code below.
@Composable
fun TextSuggest(modifier: Modifier = Modifier, text: String) {
Box(
modifier
.width(300.dp)
.wrapContentHeight()
.clip(RoundedCornerShape(20))
.background(color = blueDFEDF3)
) {
Text(
text = text,
fontSize = 12.sp,
modifier = modifier
.fillMaxWidth()
.padding(20.dp)
)
}
}
  • Now let’s create variable as mutable state with type integer to manage value changing of each emoticon when user click the Image Button, like the code below.
var feelCode by remember { mutableStateOf(0) }
  • Then, create several condition when the feelCode variable change and will show the different image. Beside image it will be show the quotes text too, like the code below.
when (feelCode) {
1 -> {
ImageTop(
image = R.drawable.sad,
modifier = modifier
.size(250.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
TextSuggest(
text = stringResource(R.string.sad_quotes),
modifier = modifier.constrainAs(textSuggest) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(emoticonImage.bottom)
})
}
2 -> {
ImageTop(
image = R.drawable.bored,
modifier = modifier
.size(250.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
TextSuggest(
text = stringResource(R.string.bored_quotes),
modifier = modifier.constrainAs(textSuggest) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(emoticonImage.bottom)
})
}
3 -> {
ImageTop(
image = R.drawable.pressure,
modifier = modifier
.size(250.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
TextSuggest(
text = stringResource(R.string.pressure_quotes),
modifier = modifier.constrainAs(textSuggest) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(emoticonImage.bottom)
})
}
4 -> {
ImageTop(
image = R.drawable.happy,
modifier = modifier
.size(250.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
TextSuggest(
text = stringResource(R.string.happy_quotes),
modifier = modifier.constrainAs(textSuggest) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(emoticonImage.bottom)
})
}
5 -> {
ImageTop(
image = R.drawable.laughing,
modifier = modifier
.size(250.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
TextSuggest(
text = stringResource(R.string.laughing_quotes),
modifier = modifier.constrainAs(textSuggest) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(emoticonImage.bottom)
})
}
6 -> {
ImageTop(
image = R.drawable.love,
modifier = modifier
.size(250.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
TextSuggest(
text = stringResource(R.string.love_quotes),
modifier = modifier.constrainAs(textSuggest) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(emoticonImage.bottom)
})
}
else -> ImageTop(
image = R.drawable.surprised,
modifier = modifier
.size(300.dp)
.constrainAs(emoticonImage) {
start.linkTo(parent.start)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
}
  • After that, assign value from each ImageButton to feelCode, so when user click the ImageButton the value will be stored on feelCode variable like the code below.
ImageButton(image = R.drawable.sad, modifier = modifier.constrainAs(sadButton) {
start.linkTo(parent.start)
top.linkTo(firstSpacer.bottom)
}, onClick = {
feelCode = 1
})
ImageButton(image = R.drawable.bored, modifier = modifier.constrainAs(boredButton) {
start.linkTo(sadButton.end)
top.linkTo(sadButton.top)
bottom.linkTo(sadButton.bottom)
}, onClick = {
feelCode = 2
})
ImageButton(
image = R.drawable.pressure,
modifier = modifier.constrainAs(pressureButton) {
start.linkTo(boredButton.end)
top.linkTo(boredButton.top)
bottom.linkTo(boredButton.bottom)
end.linkTo(parent.end)
}, onClick = {
feelCode = 3
})
ImageButton(image = R.drawable.happy, modifier = modifier.constrainAs(happyButton) {
start.linkTo(parent.start)
top.linkTo(sadButton.bottom)
}, onClick = {
feelCode = 4
})
ImageButton(
image = R.drawable.laughing,
modifier = modifier.constrainAs(laughingButton) {
start.linkTo(happyButton.end)
top.linkTo(happyButton.top)
bottom.linkTo(happyButton.bottom)
}, onClick = {
feelCode = 5
})
ImageButton(
image = R.drawable.love,
modifier = modifier.constrainAs(loveButton) {
start.linkTo(laughingButton.end)
top.linkTo(laughingButton.top)
bottom.linkTo(laughingButton.bottom)
end.linkTo(parent.end)
}, onClick = {
feelCode = 6
})
  • To make it clear between condition the emoticon expanding with question text and emoticon not expanding with quotes text. We need to create variable to stored boolean value and storing value from the expanding emoticon. Because of the default emoticon is expanding, so we need assign true value as default, like the code below.
var isExpanded by remember { mutableStateOf(true) }
  • Then, assign value from each ImageButton as false value that will be stored by isExpanded variable. Besides that we need to wrap the ImageButton with question text will show as isExpanded condition, and emoticon and quotes text as !isExpanded. Like the code below.
if (isExpanded) {
Text(
text = "What do you feel today ?",
fontSize = 16.sp,
modifier = modifier
.fillMaxWidth()
.padding(start = 40.dp, top = 20.dp, end = 20.dp)
.constrainAs(questionText) {
start.linkTo(parent.start)
top.linkTo(emoticonImage.bottom)
end.linkTo(parent.end)
})
ImageButton(image = R.drawable.sad, modifier = modifier.constrainAs(sadButton) {
start.linkTo(parent.start)
top.linkTo(firstSpacer.bottom)
}, onClick = {
feelCode = 1
isExpanded = false
})
ImageButton(image = R.drawable.bored, modifier = modifier.constrainAs(boredButton) {
start.linkTo(sadButton.end)
top.linkTo(sadButton.top)
bottom.linkTo(sadButton.bottom)
}, onClick = {
feelCode = 2
isExpanded = false
})
ImageButton(
image = R.drawable.pressure,
modifier = modifier.constrainAs(pressureButton) {
start.linkTo(boredButton.end)
top.linkTo(boredButton.top)
bottom.linkTo(boredButton.bottom)
end.linkTo(parent.end)
}, onClick = {
feelCode = 3
isExpanded = false
})
ImageButton(image = R.drawable.happy, modifier = modifier.constrainAs(happyButton) {
start.linkTo(parent.start)
top.linkTo(sadButton.bottom)
}, onClick = {
feelCode = 4
isExpanded = false
})
ImageButton(
image = R.drawable.laughing,
modifier = modifier.constrainAs(laughingButton) {
start.linkTo(happyButton.end)
top.linkTo(happyButton.top)
bottom.linkTo(happyButton.bottom)
}, onClick = {
feelCode = 5
isExpanded = false
})
ImageButton(
image = R.drawable.love,
modifier = modifier.constrainAs(loveButton) {
start.linkTo(laughingButton.end)
top.linkTo(laughingButton.top)
bottom.linkTo(laughingButton.bottom)
end.linkTo(parent.end)
}, onClick = {
feelCode = 6
isExpanded = false
})

} else {
Text(text = "Click Here for Asking Again!", fontSize = 12.sp, modifier = modifier
.clickable {
isExpanded = true
feelCode = 0
}
.padding(start = 40.dp, end = 40.dp, top = 20.dp)
.constrainAs(textAsk) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(textSuggest.bottom)
})
}
  • Call the EmoticonApp() function into the onCreate function, and compile it.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PlayingWithComposeTheme {
Surface {
EmoticonApp()
}
}
}
}
  • We already implement mutable state with mutable delegate structure to manage value of the feelCode and isExpanded. But the apps look not really nice, right? So yeah let’s modify those app with some transition. We can use isExpanded variable to be a trigger of the transision animation, like the code below.
val animateEmoticon by animateDpAsState(
targetValue = if (isExpanded) 300.dp else 250.dp, animationSpec = spring(
Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessLow
)
)
  • The last things that need to modify is the value size of emoticon image. The size before modify, for isExpanded 300.dp and !isExpended 250.dp and now change the size become animateEmoticon, and the all code will be like the below.
  • Let’s try to compile, and the app will be like this.

That’s all for this article, you can check the project code on this Github Repositories inside branch mood-quotes, hopefully useful and see you in the next article!!! Don’t forget to follow and👏🏻 for this article because it means a lot to me to be more enthusiastic about writing the next content — :).

--

--

Veronica Putri Anggraini

Software Engineer Android @LINE Bank 🤖 Google Developer Expert for Android https://github.com/veroanggra