Jetpack Compose from entry to entry (5)

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

rememberIt 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 valueare fixed as empty strings. rememberLet'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 onValueChangeupdating the value, mutableStateOfan 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.

  • LiveDatacan be converted to State using extension functions observeAsState().
  • Flowcan be converted to State using extension functions collectAsState().
  • RxJavacan be converted to State using extension functions subscribeAsState().

2.rememberSaveable

While rememberhelps you preserve state after a reorganization, it does not help you preserve state after a configuration change. For this you have to use rememberSaveable. rememberSaveableAny 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. rememberSaveableAt this point, replace can be used rememberto 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 @Parcelizecan solve the problem.

If it is unavailable for some reason @Parcelize, you can use mapSavercustom 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 listSaverand 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 rememberor rememberSaveStatemethod 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 Composabletend to be less reusable and harder to test.

When developing reusable objects Composable, you often want to provide both Composablestateful 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, Composablethe 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. LazyColumnFor example, scrolling to the specified item mentioned in the previous article puts all interactions Composablein the current one.

	val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    LazyColumn(
        state = listState,
    ) {
    
    
       /* ... */
    }

	Button(
        onClick = {
    
    
            coroutineScope.launch {
    
    
                listState.animateScrollToItem(index = 0)
            }
        }
    ) {
    
    
        ...
    }

In fact, looking at rememberLazyListStatethe 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") {
    
     /* ... */ }
        }
    }
}

rememberMyAppStatecode:

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.

reference

Guess you like

Origin blog.csdn.net/qq_17766199/article/details/123776186