State in an application is any value that can change over time. This is a very broad definition, covering everything from Room databases to class variables.
Since Compose is a declarative UI that updates the UI based on state changes, state handling is crucial. The state here can be simply understood as the data displayed on the page, then state management is to deal with the reading and writing of data.
1.remember
remember
It is used to save the state. Here is a small example.
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = "",
onValueChange = {
},
label = {
Text("Name") }
)
}
}
For example, if we add an input box to the page, if it is only handled in the above code, then you will find that the text we input will not be recorded, and the input box will always be empty. This is because properties value
are fixed as empty strings. remember
Let's optimize it using :
@Composable
fun HelloContent() {
val inputValue = remember {
mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = inputValue.value,
onValueChange = {
inputValue.value = it
},
label = {
Text("Name") }
)
}
}
By onValueChange
updating the value, mutableStateOf
an observable will be created MutableState<T>
. When the value changes, the system will reorganize all functions that read the value Composable
, which will automatically update the UI.
Jetpack Compose does not force you to use MutableState to store state. Jetpack Compose supports additional observable types. Before reading other observable types in Jetpack Compose, you must convert them to State so that Jetpack Compose can automatically restructure the interface when the state changes.
LiveData
can be converted to State using extension functionsobserveAsState()
.Flow
can be converted to State using extension functionscollectAsState()
.RxJava
can be converted to State using extension functionssubscribeAsState()
.
2.rememberSaveable
While
remember
helps you preserve state after a reorganization, it does not help you preserve state after a configuration change. For this you have to userememberSaveable
.rememberSaveable
Any values that can be saved in the Bundle are automatically saved.
Still the above example, if we rotate the screen, we will find that the text in the input box will be lost. rememberSaveable
At this point, replace can be used remember
to help us restore the state of the interface.
Since the saved data are all in Bundle
, the types of data that can be saved are limited. Such as basic types, String, Parcelable, Serializable, etc. Generally speaking, adding an annotation to the object that needs to be saved @Parcelize
can solve the problem.
If it is unavailable for some reason @Parcelize
, you can use mapSaver
custom rules to define how objects are saved and restored 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"))
}
}
If you find it cumbersome to define the key of the map, you can use listSaver
and use its index as the key.
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"))
}
}
3. Status improvement
For the above functions that use the remember
or rememberSaveState
method to save the state Composable
, we call it stateful. The benefit of being stateful is that the caller does not need to control the state, and does not have to manage the state itself. However, those with internal state Composable
tend to be less reusable and harder to test.
When developing reusable objects
Composable
, you often want to provide bothComposable
stateful and stateless versions of the same. The stateful version is convenient for callers who don't care about state, while the stateless version is necessary for callers who need to control or promote state.
State hoisting in Compose is a pattern that moves state to the caller to make composables stateless.
Let’s take an example to illustrate state improvement. For example, we implement a Dialog. For the convenience of use, we can write the displayed text and click event logic inside the dialog and encapsulate it. Although it is simple to use, it is not universal. So for general purpose, we can pass in the text and the callback of the click event as parameters, which makes it more flexible.
State promotion is actually such a programming idea, just changed the noun, there is nothing special about it.
For the example of the input box above, let's optimize it with a status prompt:
@Composable
fun HelloScreen() {
var name by rememberSaveable {
mutableStateOf("") }
HelloContent(name = name, onNameChange = {
name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = {
Text("Name") }
)
}
}
In this way, Composable
the function HelloContent is decoupled from the state storage method, which is convenient for us to reuse.
This pattern of states going down and events going up is called "unidirectional data flow". In this case, the state descends from HelloScreen to HelloContent, and the event ascends from HelloContent to HelloScreen. By following a unidirectional data flow, you decouple the composable items that display state in your UI from the parts of your app that store and change state.
4. State Management
Depending on the complexity of the composable, different alternatives need to be considered:
Use Composable as a trusted source
Used to manage the state of simple interface elements. LazyColumn
For example, scrolling to the specified item mentioned in the previous article puts all interactions Composable
in the current one.
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
state = listState,
) {
/* ... */
}
Button(
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(index = 0)
}
}
) {
...
}
In fact, looking at rememberLazyListState
the source code, you can see that the implementation is very simple:
@Composable
fun rememberLazyListState(
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
return rememberSaveable(saver = LazyListState.Saver) {
LazyListState(
initialFirstVisibleItemIndex,
initialFirstVisibleItemScrollOffset
)
}
}
Use the state container as a source of trust
When a composable contains complex UI logic that involves the state of multiple UI elements, the corresponding transaction should be delegated to the state container. Doing so makes it easier to test that logic in isolation and also reduces the complexity of the composables. This approach supports the separation of concerns principle: Composables are responsible for emitting UI elements, while state containers contain UI logic and state for UI elements.
@Composable
fun MyApp() {
MyTheme {
val myAppState = rememberMyAppState()
Scaffold(
scaffoldState = myAppState.scaffoldState,
bottomBar = {
if (myAppState.shouldShowBottomBar) {
BottomBar(
tabs = myAppState.bottomBarTabs,
navigateToRoute = {
myAppState.navigateToBottomBarRoute(it)
}
)
}
}
) {
NavHost(navController = myAppState.navController, "initial") {
/* ... */ }
}
}
}
rememberMyAppState
code:
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavHostController,
private val resources: Resources,
/* ... */
) {
val bottomBarTabs = /* State */
val shouldShowBottomBar: Boolean
get() = /* ... */
fun navigateToBottomBarRoute(route: String) {
/* ... */ }
fun showSnackbar(message: String) {
/* ... */ }
}
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
resources: Resources = LocalContext.current.resources,
/* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
MyAppState(scaffoldState, navController, resources, /* ... */)
}
In fact, it is another layer of encapsulation, and the user processes the logic. The encapsulated part is called the state container , which is used to manage the logic and state of Composable.
Use the ViewModel as a source of trust
A special type of state container used to provide access to business logic and screen or UI state.
ViewModel has a longer lifetime than Composable, so long-lived references to state bound to the composite lifetime should not be kept. Otherwise, memory leaks may result. It is recommended that screen-level Composables use ViewModels to provide access to business logic and as a source of truth for their interface state. See the ViewModel and State Containers section for an explanation of why the ViewModel is suitable for this situation.
This is the end of this article, please give me a like~ Give me a little encouragement, you can also bookmark this article for emergencies.