Table of contents
- Introduction
- States and Composition
- States in Composable
- Supported types of States
- Stateless vs Stateful
- State Hoisting
- Restoring States in Compose
- States in ViewModel.
- Step 1 - Creating the ViewModel
- Step 2 - Creating Data class for the Tasks
- Step 3- Creating the Task Item
- Step 4- Task List
- Step 5 - Mapping Data to the Screen
- Conclusion
- Futher Reading
Introduction
The state is any value that can change over time. Examples of states in android are:-
- Snackbar
- blog post with associated comments
- Ripples animations on buttons that play when a user clicks them.
- Stickers that users can draw on top of images. Here we shall learn how to connect the state with composable and APIs in jetpack compose.
States and Composition
Compose is declarative and the only way to update it is by calling the same composable with the new argument which is representative of the UI state. Anytime a state is updated a recomposition takes place thus composable like TextFields
don't automatically update like in XML
views.
- Composition - a description of the UI built by jetpack compose when it executes composable.
- initial Composition - Is the creation of a composition by running composable the first time.
- Re-composition - Re-running composable to update the composition when data changes.
States in Composable
Composable functions stores a single object in memory by using the remember
composable. A value computed by remember is stored in the composition during initial composition and returned during recomposition.remember
can be used to store both mutable and immutable objects.
Any change to a value will schedule the recomposition of any composable function that read the value. In the case of ExpandingCard
when expanded it causes ExpandingCard
to be recomposed.
Ways to declare MutableStates objects in Compose
- val mutableState = remember{mutableStateOf("default")}
- val value by remember = {mutableStateOf("default")}
- val (value,setValue) = remember{mutableStateOf("default")}
when using by keyword ensure you have the following imports
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Remember value can be used as parameter of other composable or even as logic in statement to change which composable is displayed example
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
Remember only retain state during composition and during configuration like screen rotation the state is not retained. For this case, we use rememberSaveable
which automatically saves any value stored in a bundle.
Supported types of States
Jetpack compose supports other observables types which should be converted to State<T>
so that compose can automatically recompose when there is a state change.
Compose only automatically recompose by reading the State<T>
object from the observable.
Mutable objects that are not observable, such as ArrayList<T>
or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.
Instead of using non-observable mutable objects, we recommend you use an observable data holder such as State<List<T>>
and the immutable listOf().
Stateless vs Stateful
Stateful uses remember
to store an object and creates an internal state, making the composable stateful.
This can be useful in situations where a caller doesn't need to control the state and can use it without having to manage the state themselves.
However, composable with an internal state tends to be less reusable and harder to test.
A stateless composable is composable that doesn't hold any state. An easy way to achieve statelessness is by using state hoisting.
State Hoisting
It's a pattern of moving states to a composable's caller to make a composable stateless. The general pattern used is by replacing the state variable with two parameters.
Example
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
//displays the count
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
//owns the count
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
Properties of Hoisted States
- Single source of truth - By moving state instead of duplicating it, we're ensuring there's only one source of truth. This helps avoid bugs.
- Encapsulated - Only stateful composable will be able to modify their state. It's completely internal.
- Shareable - A hoisted state can be shared with multiple composable
- Interceptable - callers to the stateless composable can decide to ignore or modify events before changing the state.
- Decoupled - the state for the stateless ExpandingCard may be stored anywhere. For example, it's now possible to move a name into a ViewModel
Rules to figure out where states should go during hoisting
- State should be hoisted to at least the lowest common parent of all composable that use the state (read).
- State should be hoisted to at least the highest level it may be changed (write).
- If two states change in response to the same events they should be hoisted together.
Restoring States in Compose
rememberSaveable
retains state across recompositions and also retains state across activity and process recreation.
Other Ways to Restore State
Data that are bundled are saved automatically when working with data that is not bundled use the following
- Parcelize - Add the @Parcelize annotation to the object. To make it parcelable and bundled.
@Parcelize
data class City(
val name: String,
val country:String
): Parcelable
- MapSaver - It's used to define your own rule for converting an object into a set of values that the system can save to the bundle.
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
- ListSaver To avoid defining the keys for the map, you can use listSaver and use its indices as keys
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
States are managed with the following:-
- For Hoisted states, they are managed by the
Composable
for simple UI elements. - State holder this applies to complex UI. They own the UI elements states and logic.
- Architecture component - They provide access to the business logic and the UI state.
States in ViewModel.
Screen or Ui state indicates what should be displayed on the screen. The state is usually connected with other layers in the hierarchy because it contains application data.
The logic of the application describes how the application should behave and reacts to state changes.
Type of logic
- UI Logic - Shows how to display state changes on the UI example is navigation logic.
- Business Logic - Explains what to do with state changes. example making payments or storing user preferences.
Let's get started with implementation.
Step 1 - Creating the ViewModel
ViewModel provides Ui state and access to the business logic located on other layers of the app. ViewModel survives configuration changes and thus has a longer lifetime than the composition.
Ensure to add the gradle
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
Step 2 - Creating Data class for the Tasks
This class holds all the data used by the ViewModel.
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
Step 3- Creating the Task Item
This simply maps how the items will appear on the screen.
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
Step 4- Task List
This is a list that holds all the task items under one LazyColumn
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
Step 5 - Mapping Data to the Screen
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
Finally call the Function to the main activity and run the program.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StatesDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
WellnessScreen()
}
}
}
}
}
Conclusion
In this article, we have gone through states in compose and all the definitions used. Supported types of States, Stateless vs Stateful. State Hoisting, Restoring States in Compose and States in ViewModel.