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")
}
}
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:
-
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") } }
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"
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")
}
}
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
}
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= {})
}
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")
}
}
Now you can use ClickCounter with state or without state if you want.
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
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.
Top comments (0)