Explain in detail the state management and use in Jetpack Compose

foreword

  To quote an official description, as follows

Since Compose is a declarative toolset, the only way to update it is to call the same composable with new parameters. These parameters are the representation of the interface state. Whenever the state is updated, a reorganization occurs. Therefore, the TextField will not update automatically as it does in the XML-based imperative view. Composables must be explicitly informed of the new state in order to be updated accordingly.

  What Google wants to express is that compose will not be like xml layout, you can simply call methods in the code actively (such as setText(), setImageResource(), etc.) to refresh the UI content, but need to notify the UI through state management The content needs to be updated. 

  The state management of Compose is more in line with the MVVM idea, although Jetpack has launched ViewModel, LiveData, MutableLiveData, and DataBinding (this is the most poisonous, creating a precedent for XML writing logic) as state management long ago. But because of the positioning of XML and Activity, it is difficult to say that the current Android programming is the MVVM model, it can only be said to be close.

  The embarrassing reasons are:

  1.XML is written for the implementation of the View layer, but it cannot update and control the View layer. And the style of XML already shows that it is not suitable for writing logic to control View.

  2. Activity is not only the View layer controller, but also does not realize the code writing of View.

    They were supposed to be one, but they were separated. As a result, MVC, MVP, and MVVM ideas cannot fully fit the Android platform, and many newcomers often get confused when learning to use these three ideas on the Android platform.

State management involves classes and methods

  • remember : Save the data, and the saved value will be provided when the UI is updated. But the saved value will be lost after the Activity page exits
  • rememberSaveable : Save the data and write the value into the bundle, and then read the data from the bundle when rebuilding the Activity. This means that the value will not be lost after the Activity exits.
  • mutableStateOf  : A state store that is mutable and observed by Compose at all times. The function is to let Compose know that the data has changed, and the content on the UI needs to be redrawn.
  • mutableStateListOf : mutableStateOf can only observe changes in a single type of data, but cannot observe changes in collection data. So with mutableStateListOf, the method parameter has the vararg keyword, so it can also be an array composed of multiple Lists
  • mutableStateMapOf : Same as above, except that it is in the form of a hash, and the method parameter has the vararg keyword, so it can also be an array
  • derivedStateOf: When the defined object state depends on other object states, it needs to be used derivedStateOf. When the state of the dependent object changes, it can also change itself.

After reading the above, you can understand that remember is used to temporarily save data, and MutableState is used to notify and transfer data changes.

An example of the use of remember and mutableStateOf  (a quick demo)

Implement a Demo that automatically increments the value and displays it when a button is clicked. Generally, mutableStateOf and remember are used together (but they are not bound and can be used alone). What is the difference between using mutableStateOf directly and combining it with remember? Please see the "Why mutableStateOf can't be directly written into the method inside the example" at the back of the blog. However, it is recommended that you keep your questions and read them in order.

The following code shows three creation methods, but these three methods are different syntactic sugar, and the result is the same.

code:

class DeploymentActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyButton()
        }
    }

    @Preview(name = "按键自增计数")
    @Composable
    fun MyButton() {
        Column() {
            /*
            使用by需要引用
            import androidx.compose.runtime.getValue
            import androidx.compose.runtime.setValue
            */
            var count1 by remember { mutableStateOf(0) }
            Button(onClick = { count1++ }) {
                Text(text = "按键A = $count1")
            }

            var count2 = remember { mutableStateOf(0) }
            Button(onClick = { count2.value++ }) {
                Text(text = "按键B = ${count2.value}")
            }

            var (count3, setValue) = remember { mutableStateOf(0) }
            Button(onClick = { setValue.invoke(count3+1) }) {
                Text(text = "按键C = $count3")
            }
        }
    }
}

Effect animation:

Example of using mutableStateListOf

mutableStateListOf is used in the case of collection data. It can trigger reorganization when the collection data changes, because if mutableStateOf is used, the change of the collection data will not be observed, thus not triggering reorganization.

private var mImageList = mutableStateListOf<String>()
    @Composable
    private fun collectContentList() {
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = 256.dp),
            verticalArrangement = Arrangement.spacedBy(20.dp),
            horizontalArrangement = Arrangement.spacedBy(20.dp),
            contentPadding = PaddingValues(top = 20.dp, start = 20.dp, end = 20.dp)
        ) {
            items(mImageList.size) { index ->
                AsyncImage(model = mImageList[index],
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .width(256.dp)
                        .height(128.dp)
                        .pointerInput(Unit) {
                            detectTapGestures(
                                onTap = {
                                    DrawFromActivity.jumpCarryFileImage(
                                        context = requireContext(),
                                        mImageList[index]
                                    )
                                },
                                onLongPress = {
                                    mCurrentDeleteImagePath = mImageList[index]
                                    mIsShowDeleteDialog.value = true
                                }
                            )
                        }
                        .clip(RoundedCornerShape(10.dp)))
            }
        }
    }

 

Example of using mutableStateMapOf

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyMapList()
        }
    }


    @Composable
    fun MyMapList() {
        val dataMap = remember {
            mutableStateMapOf("key1" to "value1",
            "key2" to "value2",
            "key3" to "value3",
            "key4" to "value4")
        }
        LazyColumn {
            items(dataMap.size){ index->
                val itemKey = dataMap.keys.toMutableList()[index]
                val itemValue = dataMap[itemKey]
                itemValue?.let { Text(text = it) }
            }
        }
    }

An example of using derivedStateOf

 The usage scenario of derivedStateOf is that a certain data needs to rely on calculation or derivation of other state management.

The code example is as follows:

We need to count, and the result of the count derives a new requirement to judge whether it is odd or even.

    @Preview
    @Composable
    fun MyText() {
        val count = remember { mutableStateOf(0) }
        //是否是奇数
        val isOddNumber = remember {
            derivedStateOf {
                count.value % 2 != 0
            }
        }

        Text(text = "计数 = ${count.value} 是否是奇数 = ${isOddNumber.value}",
            color = Color.White,
            modifier = Modifier.clickable {
            count.value++
        })
    }

result:

An example of using remember with parameters

  The use example of remember without parameters has been explained above, and the example will not be repeated. Now let's talk about the example of using remember with parameters.

  The code block of remember will only be executed once when it is created for the first time, and will not be executed later. What if we have a need to execute the remember code block once when the Compose method is reorganized? Then you need to use remember with parameters, as long as you change the key, remember will re-execute the code block when compose is reorganized.

To give a negative reference, a code example without parameters:

    @Composable
    fun MyText() {
        //这个count是用来触发整个方法重组的
        val count = remember { mutableStateOf(0) }
        //不添加key
        val randomNum = remember() {
            Log.e("zh", "remember被重新执行代码块了")
            (0..99).random()
        }

        Text(text = "按键A = ${count.value}", modifier = Modifier.clickable {
            count.value++
            Log.e("zh", "randomNum = ${randomNum}")
        })
    }

 After clicking Text, the result of the reorganization can be seen that the random number has not changed, it is fixed at 51, and the log in the remember code block is not printed.

Example with parameters:

Please note that because remember is inside Compose, if you want to re-execute the code block with parameter remember, you need to let Compose reorganize, so the following count is used to trigger the reorganization.

    var key = 0

    @Composable
    fun MyText() {
        //这个count是用来触发整个方法重组的
        val count = remember { mutableStateOf(0) }
        //添加key
        val randomNum = remember(key) {
            Log.e("zh", "remember被重新执行代码块了")
            (0..99).random()
        }

        Text(text = "按键A = ${count.value}", modifier = Modifier.clickable {
            key++
            count.value++
            Log.e("zh", "key = ${key}")
            Log.e("zh", "randomNum = ${randomNum}")
        })
    }

As a result of the reorganization after clicking Text, the remember of randomNum can also be triggered to re-execute the code block because of the key change. Thus updating the value of the random number.

Example of using rememberSaveable

Save the data, and write the value into the bundle, and then read the data from the bundle when rebuilding the Activity. This means that the value will not be lost after the Activity exits.

 code:

    @Composable
    fun MyText() {
        val count = rememberSaveable {
            mutableStateOf(0)
        }

        Text(text = "计数 = ${count.value}  ",
            color = Color.Black,
            modifier = Modifier.clickable {
                count.value++
            })
    }

Understanding MutableState Reorganizes UI Component Scope

mutableState restructures the scope of the UI component in which it reads and writes. To test this statement, consider the following code example:

Example 1 of reorganizing UI scope under the combination of multiple @Composable methods:

In the following code, after the Column is clicked, the value of count is increased but it does not cause any UI reorganization. Because none of the three Text references count.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val count = remember { mutableStateOf(1) }
            Column(modifier = Modifier.clickable { count.value++ }) {
                Log.e("zh", "Column触发重组")
                Text1()
                Text2()
                Text3()
            }
        }
    }

    @Composable
    fun Text1() {
        Log.e("zh", "Text1触发重组")
        Text(text = "测试")
    }

    @Composable
    fun Text2() {
        Log.e("zh", "Text2触发重组")
        Text(text = "测试")
    }

    @Composable
    fun Text3() {
        Log.e("zh", "Text3触发重组")
        Text(text = "测试")
    }

Example 2 of reorganizing UI scope under the combination of multiple @Composable methods:

In the following code, Text1 refers to the count data, so after clicking Columu to increase the count value, the scope of reorganization is only in the custom Text1 method, and the external Column does not trigger reorganization.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val count = remember { mutableStateOf(1) }
            Column(modifier = Modifier.clickable { count.value++ }) {
                Log.e("zh", "Column触发重组")
                Text1(count)
                Text2(count)
                Text3(count)
            }
        }
    }

    @Composable
    fun Text1(count: MutableState<Int>) {
        Log.e("zh", "Text1触发重组 count = ${count.value} count内存地址= ${count}")
        Text(text = "测试 ${count.value}")
    }

    @Composable
    fun Text2(count: MutableState<Int>) {
        Log.e("zh", "Text2触发重组")
        Text(text = "测试")
    }

    @Composable
    fun Text3(count: MutableState<Int>) {
        Log.e("zh", "Text3触发重组")
        Text(text = "测试")
    }

Example of recombination scope inside a @Composable method:

 All components inside the method are reorganized after the Text is clicked. However, there are special cases where reorganization is not triggered within the entire method in all cases. When Button, Surface, and CompositionLocalProvider are called, the scope of reorganization will only be limited to the interior of these components (in fact, Button and Surface contain CompositionLocalProvider, and the resulting reorganization will only limited to their range)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyButton()
        }
    }

    @Composable
    fun MyButton() {
        val count = remember { mutableStateOf(1) }
        Log.e("zh", "触发重组1")
        Column {
            Log.e("zh", "触发重组2")
           Column {
               Log.e("zh", "触发重组3")
               Text(text = "数值 = ${count.value}", modifier = Modifier
                   .width(100.dp)
                   .height(100.dp)
                   .clickable { count.value++ })
           }
        }
    }

Example of recombination scope of CompositionLocalProvider inside @Composable method:

Button and Surface contain CompositionLocalProvider inside, so give an example together. In the following code, after clicking any component to increase the Count value, any log under Column will not be triggered, because the scope of reorganization is limited to CompositionLocalProvider.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyButton()
        }
    }


    @OptIn(ExperimentalMaterialApi::class)
    @Composable
    fun MyButton() {
        val count = remember { mutableStateOf(1) }
        Log.e("zh", "触发重组1")
        Column {
            Log.e("zh", "触发重组2")
            Column {
                Log.e("zh", "触发重组3")
                Button(onClick = { count.value++ }) {
                    Log.e("zh", "Button触发重组")
                    Text(text = "Button = ${count.value}")
                }
                CompositionLocalProvider(){
                    Log.e("zh", "CompositionLocalProvider触发重组")
                    Text(text = "CompositionLocalProvider = ${count.value}", modifier = Modifier
                        .width(100.dp)
                        .height(100.dp)
                        .clickable { count.value++ })
                }
                Surface(onClick = { count.value++ },modifier = Modifier
                    .width(100.dp)
                    .height(100.dp)) {
                    Log.e("zh", "Surface触发重组")
                    Text(text = "Surface = ${count.value}")
                }
            }
        }
    }

Why can't mutableStateOf be directly written to the example inside the method:

In the above example, all the externals that create mutableStateOf have a remember set. Then some people will definitely ask, why add remember? What about not creating mutableStateOf directly inside the method? In fact, the key to this problem is to understand the reorganization of components. Because every reorganization of the component method will cause mutableStateOf to be recreated once. The text of remember means to remember, so the function of remember is to refer mutableStateOf or other entity data to the SlotTable saved in each Compose, without being affected by its reorganization.

 In the following code, we deliberately create mutableStateOf directly in the component method. Let's see what problems will be caused after reorganization after the Count is auto-incremented after the Text is clicked:

    @SuppressLint("UnrememberedMutableState") //在内部调用mutableStateOf会出现警告
    @Composable
    fun MyButton() {
        val count = mutableStateOf(1)
        Log.e("zh", "count地址 = ${count}")
        Column {
            //因为Button含有CompositionLocalProvider不会导致外部也触发重组,所以这里用Text替代
            Text(text = "按键A = ${count.value}", modifier = Modifier.clickable { count.value++ })
            Log.e("zh", "count = ${count.value}")
        }
    }

The result is that each reorganization of the component method also recreates the MutableState, so that the value will not increase automatically, and the memory address is new every time.

But mutableStateOf can be written externally . In the following code, mCount1 is saved in the global variable of the Activity class, and count2 is saved in the SlotTable of Compose created by Composable, but there is no big difference between the two in use.

    val mCount1 = mutableStateOf(1)

    @Composable
    fun MyText() {
        val count2 = remember { mutableStateOf(1) }
        Column {
            Text(text = "mCount1 ${mCount1}")
            Text(text = "count2 ${count2}")
        }
    }

MutableState notifies the UI reorganization mechanism

Here you can use the picture below to have a brief understanding... The mechanism of MutableState is quite complicated, and it is very brain-burning to understand it in depth. Because code tracking is not easy to use, you have to use debug to find out the sending and receiving of their observer messages. I personally think that as long as you understand SnapshotMutableStateImpl, you can simply understand the mechanism of State and Snapshot.

The principle of remember

The following uses the source code method to show the process of remember and see where remember caches the data.

 Source code one

 

/**
 * 记住高阶函数calculation执行后产生的值。重组将总是返回产生的值
 */
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

Source code two

/**
 * A Compose compiler plugin API. DO NOT call directly.
 * 缓存记录,一个组合的组合数据中的值。编译器插件使用它来生成更有效的调用,以便在确定这些操作是安全的时候进行记录。
 */
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
    @Suppress("UNCHECKED_CAST")
    return rememberedValue().let {
        if (invalid || it === Composer.Empty) {
            val value = block()
            updateRememberedValue(value)
            value
        } else it
    } as T
}

Source code three

   override fun updateRememberedValue(value: Any?) = updateValue(value)

Source code four

    /**
     * 将SlotTable的值更新为[value]的当前值。
     *
     * @param value the value to schedule to be written to the slot table.
     */
    @PublishedApi
    @OptIn(InternalComposeApi::class)
    internal fun updateValue(value: Any?) {
        if (inserting) {
            //插入新的值
            writer.update(value)
            if (value is RememberObserver) {
                record { _, _, rememberManager -> rememberManager.remembering(value) }
                abandonSet.add(value)
            }
        } else {
            //更新已经存在的值
            val groupSlotIndex = reader.groupSlotIndex - 1
            if (value is RememberObserver) {
                abandonSet.add(value)
            }
            recordSlotTableOperation(forParent = true) { _, slots, rememberManager ->
                if (value is RememberObserver) {
                    rememberManager.remembering(value)
                }
                //这里的set方法可以将新值保存到SlotTable里,并且将旧的值返回
                when (val previous = slots.set(groupSlotIndex, value)) {
                    is RememberObserver ->
                        //观察者记录管理类,将以前的注册的RememberObserver观察者移除
                        rememberManager.forgetting(previous)
                    //重组范围实施类
                    is RecomposeScopeImpl -> {
                        val composition = previous.composition
                        if (composition != null) {
                            //释放之前的值
                            previous.release()
                            //设置当前composition失效范围
                            composition.pendingInvalidScopes = true
                        }
                    }
                }
            }
        }
    }

what is the meaning of remember

      Most of it has been explained in the "Example of why mutableStateOf cannot be written directly inside the method" above the article. I’m going to repeat it here, the meaning is to save a copy of the data that needs to be cached for each Compose, so that it will not be affected by Compose reorganization. This design is because the application of the mobile platform has the need to switch the front and back, so there is a concept of the page life cycle. Compose needs to cache a copy of data for data restoration and display after switching between front and back.

Guess you like

Origin blog.csdn.net/qq_39312146/article/details/130738223