DEV Community

loading...
Cover image for 03 Compose - Managing State in Compose

03 Compose - Managing State in Compose

one_past_last_jedi profile image One Past Last Jedi Updated on ・4 min read

What is a State?

It is any value that can change over time in App.

Composition and recomposition and State:

During initial composition, Compose will keep track of the composables that you call to describe your UI in a composition. Then, when the state of your app changes, Jetpack Compose schedules recomposition. Recomposition is running the composables that may have changed in response to state changes, and Jetpack Compose updates the composition to reflect any changes.
A composition can only be produced by an initial composition and updated by recomposition. The only way to modify a composition is through recomposition.

How to Introduce a State or Put memory inside Composable function:

Lets see this code:

class MainActivity : ComponentActivity()
{
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContent {
            ClickCounter()
        }
    }
}

@Composable
fun ClickCounter()
{
    var clicks: Int by remember  { mutableStateOf(value = 0) }

    Button(onClick = { clicks++ })
    {
        Text(text= "I've been clicked $clicks times")
    }
}
Enter fullscreen mode Exit fullscreen mode

To put memory inside Composable function, we use remember keyword and to observe any changes in this memory we use mutableStateOf(value= initial value) and we put initial value inside the observable.

remember{}

is a function that gives composable function memory.

mutableStateOf()

creates a MutableState, which is an observable type in Compose. Any changes to its value will schedule recomposition of any composable functions that read that value.

However,

This remember{} follow the life cycle of the main Activity, so what is the solution of we want to reserve this memory even beyond life cycle of the main Activity?!

There are two solutions:

  1. Save this memory in Bundle by using rememberSaveable{} instead of remember{} like this:

    class MainActivity : ComponentActivity()
    {
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContent {
            ClickCounter()
        }
    }
    }
    @Composable
    fun ClickCounter()
    {
    var clicks: Int by rememberSaveable  { mutableStateOf(value = 0) }
    
    Button(onClick = { clicks++ })
    {
        Text(text= "I've been clicked $clicks times")
    }
    }
    
  2. By using ViewModel like this:
    First, you need to add the following in build.gradle(:app) file

implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.4"
implementation "androidx.compose.runtime:runtime-livedata:1.0.0-beta04"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04"
Enter fullscreen mode Exit fullscreen mode
class MainActivity : ComponentActivity()
{
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContent {
            ClickCounter()
        }
    }
}

class ClickCounterViewModel: ViewModel()
{
    private val _clicks = MutableLiveData(0)
    val clicks: LiveData<Int> = _clicks

    fun onClick(newClick: Int)
    {
        _clicks.value = newClick + 1
    }

}

@Composable
fun ClickCounter(clickCounterViewModel: ClickCounterViewModel = viewModel())
{
    val clicks: Int by clickCounterViewModel.clicks.observeAsState(0)

    Button(onClick = { clickCounterViewModel.onClick(clicks) })
    {
        Text(text= "I've been clicked $clicks times")
    }
} 
Enter fullscreen mode Exit fullscreen mode

Notice that Compose follow the unidirectional data flow pattern where state flows down from ClickCounterViewModel, and events flow up from ClickCounter.

However, There is a Problem?!!

When a composable holds its own state like in the example above, it makes the composable hard to reuse and test, and it also keeps the composable tightly coupled to how its state is stored.

Then, what is the solution?!

The solution is by something called state hoisting.

What is state hoisting?!

State hoisting is a pattern of moving state up the tree to make that composable function stateless. A simple way to do this is by replacing the state with a parameter and using lambdas to represent events.

In Other words,

you make two overloads composable function (and that is one advantage of Kotlin not like Dart which you cannot do overloads function in Dart)

First Overload Composable function

you make it stateless Composable function like this:

@Composable
fun Foo(value: Type, onValueChange: (Type) -> Unit)
{
    \\ your code here
}
Enter fullscreen mode Exit fullscreen mode

Where value is what was the state.

Second Overload Composable function

you make it stateful Composable function by put the state inside it and then call the stateless composable function from inside stateful composable function like this:

@Composable
fun Foo()
{
    \\ the state is here
    Foo(value= state, onValueChange= {})
}
Enter fullscreen mode Exit fullscreen mode

And the following is real example of State hoisting of ClickCounter() composable function that we do above:

class MainActivity : ComponentActivity()
{
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContent {
            ClickCounter()
        }
    }
}

class ClickCounterViewModel: ViewModel()
{
    private val _clicks = MutableLiveData(0)
    val clicks: LiveData<Int> = _clicks

    fun onClicksChange(newClicks: Int)
    {
        _clicks.value = newClicks + 1
    }

}

@Composable
fun ClickCounter(clickCounterViewModel: ClickCounterViewModel = viewModel())
{
    val clicks: Int by clickCounterViewModel.clicks.observeAsState(0)

    ClickCounter(clicks = clicks, onClicksChange =
    {clickCounterViewModel.onClicksChange(it)})
}

@Composable
fun ClickCounter(clicks: Int, onClicksChange: (Int) -> Unit)
{
    Button(onClick = { onClicksChange(clicks) })
    {
        Text(text= "I've been clicked $clicks times")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you can use ClickCounter with state or without state if you want.
Alt Text

Important:

State should be modified by events in a composable. If you modify state when running a composable instead of in an event, this is a side-effect of the composable, which should be avoided.

Finally:

You need to import the following in the above examples:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.material.*
import androidx.compose.runtime.getValue
import androidx.lifecycle.ViewModel
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewmodel.compose.viewModel
Enter fullscreen mode Exit fullscreen mode

Summary

Composition =

a description of the UI built by Jetpack Compose when it executes composables.

Initial composition =

creation of a Composition by running composables the first time.

Recomposition =

re-running composables to update the Composition when data changes.

Remember =

stores objects in the composition, and forgets those objects when the composable that called remember is removed from the composition.

================================================

You can join us in the discord server
https://discord.gg/TWnnBmS
and ask me any questions in (#kotlin-and-compose) channel.

Table of Contents

Previous Class

Next Class

Discussion (0)

pic
Editor guide