Jetpack Compose Stateful Composables, Explained
Jetpack Compose is a modern UI toolkit that uses the so-called Declarative Programming to create UIs for Android apps.
Compose is fundamentally different from XML layouts. Actually, Compose needs a different mental model. To get an idea about what I mean by a different mental model, I strongly recommend reading Thinking in Compose.
There are some fundamental concepts one needs to know in order to master Compose: recomposition, state, etc. In this post, we are going to talk about State by implementing a simple example through an iterative approach.
The “Counter”
We are going to implement a counter using Compose. Our counter consists of two parts:
- a label showing the value of the counter
- a button that when clicked increments the value of the counter.
Iteration 0
In this iteration, we just implement the overall structure of the counter. The following code snippet shows the counter.
As can be seen, a Text
composable is used to display a label and a Button
composable to display a button. Also, there are some log statements to see when the Counter0
composable runs and when the button is clicked.
If we run the above code and click on the “Increment” button 3 times nothing happens UI-wise (as we expect) and the following lines will be printed in the LogCat:
Counter0 ran
Button clicked
Button clicked
Button clicked
So far, so good.
Iteration 1
In this iteration, we introduce a local variable counter
to keep track of the value of the counter. We display the value of counter
on the label and increment it when the “Increment” button is clicked.
The following code shows our second attempt to create the counter.
If we run the above code and click on the “Increment” button 3 times nothing in the UI changes and the following lines will be printed in the LogCat:
Counter1 ran
Button clicked. Counter = 1
Button clicked. Counter = 2
Button clicked. Counter = 3
As the logs show Counter1
composable is run once. When the Text
composable creates the label, counter
is 0 hence the label displays “0”.
Clicking on the “Increment” button increments the value of counter
but the label is not updated and it always displays “0”. The reason is that although counter
increments it doesn’t cause the Text
composable to display the new value.
What we want is that whenever the value of counter
increments we want Counter1
to rerun (aka recompose) so that the Text
composable uses the new value of counter
to display the label.
Iteration 2
In this iteration, we change the type of counter
from Int
to MutableState<Int>
. The following code shows our new counter.
MutableState
is an observable type in Compose. It has a single property named value
. Any changes to value
will schedule recomposition of any composable function that reads value
.
Let's see how MutableState
works by going through the source code of Counter2
.
On line 12 we are reading the value of counter
to use it as the text of our counter’s label. Also on line 17, we are incrementing the value of counter
. Because counter
is of type MutableState
then whenever we write to it every composable function which is using the value of counter
is recomposed. So clicking on the “Increment” button increments (or changes) the value of counter
and this causes the Counter2
composable function to recompose.
If we run the above code and click on the “Increment” button 3 times, we expect our counter to show “1”, “2” and then “3”, but it always displays “0”. Also, the following lines will be printed in the LogCat:
Counter2 ran
Button clicked. Counter = 1
Counter2 ran
Button clicked. Counter = 1
Counter2 ran
Button clicked. Counter = 1
Counter2 ran
The reason why the label still displays “0” is as follows:
- Clicking on the “Increment” button increments the value of
counter
and it becomes 1. Becausecounter
is of typeMutableState
and its value has changed,Counter2
composable function is recomposed. - When
Counter2
is recomposed, a new instance ofcounter
is created with an initial value of 0 and theText
composable displays the value ofcounter
(which is 0).
So basically, every recomposition of Counter2
instantiates a new counter
with an initial value of 0 and the label of our counter always displays “0”.
What we want to achieve is to recompose the Counter2
function every time the “recomposed” button is clicked without resetting the value of counter
.
Iteration 3 (Final Iteration)
Now we can see the final version of our counter.
The only difference between this version and Counter2
is that the following line
val counter: MutableState<Int> = mutableStateOf(0)
is replaced with
val counter: MutableState<Int> = remember { mutableStateOf(0) }
By using the remember
function, we have given memory to Counter3
meaning that when Counter3
is recomposed, it remembers the last value that was stored in counter
.
As before, If we run the above code and click on the “Increment” button 3 times, the counter shows “1”, “2” and then “3”, and the following lines will be printed in the LogCat:
Counter3 ran
Button clicked. Counter = 1
Counter3 ran
Button clicked. Counter = 2
Counter3 ran
Button clicked. Counter = 3
Counter3 ran
You can see from the logs that the value of counter
is retained when Counter3
is recomposed and because of this our counter is working as expected, finally!
Side Note
If you are familiar with C then using remember
to retain a value in a composable is like defining a static variable inside a function in C using the static
keyword.
Take a look at the following code.
The output of the above code is:
6
6
This is because every execution of foo
creates a new instance of x
with an initial value of 5. Now, If we replace int x = 5
with static int x = 5
and run the code, the output will be
6
7
Herefoo
is executed twice. In the first run, x
is incremented, and “6” is printed. In the second run, because the value of x
is retained, x
is incremented again, and “7” is printed.
That’s all about creating a stateful composable in Compose. To learn more about state and recomposition take a look at State and Jetpack Compose.
If you find this post useful then clapping is appreciated (1, 2, or maybe 50 claps!)