Compose中的remember和mutableStateOf

5,628 阅读4分钟

在Compose的官方指导和示例代码中经常会看到这样的代码

 var count by remember{mutableStateOf(0)}
 或者
 var count = remember{mutableStateOf(0)}

首先需要注意几个问题

  1. 用by的话,count是Int类型【即mutableStateOf参数的类型】
  2. 用“=”, count是MutableState类型,使用时需要通过.value来取值
  3. 使用by的时候可能会报红,错误是

Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate 解决方法很简单,导包即可

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
也可以直接:
import androidx.compose.runtime.*

这个问题可能是Compose的一个bug,如果只是 by mutableStateOf(0),也会报错,但是按alt+enter 会提示需要导包,但是加了remember之后,按alt+enter是不提示导包的,非常奇怪。 4. 注意remember 后面是 {} , 不是()

进入正题,来看看mutableStateOf 和 remember 都是干嘛的

普通参数
class MainActivity : ComponentActivity() {
    @ExperimentalMaterialApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            println("---- clicked onCreated setContent ")
            Surface() {
                var count = 0 // 无状态
                Button(
                    onClick = { count++ }, modifier = Modifier
                        .padding(16.dp)
                        .fillMaxWidth()
                        .height(50.dp)
                ) {
                    Text(
                        text = "I have been clicked $count times",
                        modifier = Modifier.align(Alignment.CenterVertically)
                    )
                    SideEffect(effect = { println("---- text count = $count ") })
                }
                SideEffect(effect = { println("---- out count = $count ") })
            }
        }
    }
}

点击按钮,数字不变,控制台只有一次打印

2021-05-26 10:21:45.248 32194-32194/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 10:21:45.301 32194-32194/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 10:21:45.301 32194-32194/com.shakespace.compose I/System.out: ---- out count = 0 
有状态参数
var count by mutableStateOf(0) // 改成mutableStateOf

点击按钮,数字会不断增加,控制台输出

2021-05-26 10:26:36.949 16988-16988/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 10:26:37.002 16988-16988/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 10:26:37.002 16988-16988/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 10:26:40.511 16988-16988/com.shakespace.compose I/System.out: ---- text count = 1 
2021-05-26 10:26:41.141 16988-16988/com.shakespace.compose I/System.out: ---- text count = 2 
2021-05-26 10:26:41.792 16988-16988/com.shakespace.compose I/System.out: ---- text count = 3 
2021-05-26 10:26:42.828 16988-16988/com.shakespace.compose I/System.out: ---- text count = 4 
2021-05-26 10:26:43.596 16988-16988/com.shakespace.compose I/System.out: ---- text count = 5 
  1. 相比普通变量,使用mutableStateOf,使得变量有了被观察的能力,当值发生变化时就会通知使用这个变量的控件进行更新。
  2. 问题是这里没有用到remember,点击时数字还是会增加,是不是不需要remember呢?
重绘机制

把Button 的padding 改成和count 有关

 setContent {
            println("---- clicked onCreated setContent ")
            Surface() {
                var count by mutableStateOf(0)
                Button(
                    onClick = { count++ }, modifier = Modifier
                        .padding(count.dp) // 把Button的padding 改成和count 有关
                        .fillMaxWidth()
                        .height(50.dp)
                ) {
                    Text(
                        text = "I have been clicked $count times",
                        modifier = Modifier.align(Alignment.CenterVertically)
                    )
                    SideEffect(effect = { println("---- text count = $count ") })
                }
                SideEffect(effect = { println("---- out count = $count ") })
            }
        }

点击按钮,数字不会变化,控制台输出

2021-05-26 11:49:28.394 19077-19077/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 11:49:28.450 19077-19077/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 11:49:28.450 19077-19077/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 11:49:33.240 19077-19077/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 11:49:33.240 19077-19077/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 11:49:34.123 19077-19077/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 11:49:34.123 19077-19077/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 11:49:35.776 19077-19077/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 11:49:35.776 19077-19077/com.shakespace.compose I/System.out: ---- out count = 0 

这时候数字不会变化,是因为Surface的直接子组件Button依赖于count这个state,那么count变化的时候,Surface接收的这个Composable函数就会重绘[方法重新调用],每次调用count就会是0,所以数字没有变化。

而前面Button不依赖于count的例子,外部不需要重绘(看日志 out 只打印了一次),count也不会重置,所以数字会增加。

remember

在这种情况下,如果还想记住变量值,就要用到remember

        setContent {
            println("---- clicked onCreated setContent ")
            Surface() {
                var count by remember{ mutableStateOf(0)} // 使用remember
                Button(
                    onClick = { count++ }, modifier = Modifier
                        .padding(count.dp)
                        .fillMaxWidth()
                        .height(50.dp)
                ) {
                    Text(
                        text = "I have been clicked $count times",
                        modifier = Modifier.align(Alignment.CenterVertically)
                    )
                    SideEffect(effect = { println("---- text count = $count ") })
                }
                SideEffect(effect = { println("---- out count = $count ") })
            }
        }

点击按钮,数字会增加,控制台输出:

2021-05-26 11:56:28.675 5449-5449/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 11:56:28.728 5449-5449/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 11:56:28.729 5449-5449/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 11:56:36.311 5449-5449/com.shakespace.compose I/System.out: ---- text count = 1 
2021-05-26 11:56:36.311 5449-5449/com.shakespace.compose I/System.out: ---- out count = 1 
2021-05-26 11:56:36.840 5449-5449/com.shakespace.compose I/System.out: ---- text count = 2 
2021-05-26 11:56:36.840 5449-5449/com.shakespace.compose I/System.out: ---- out count = 2 
2021-05-26 11:56:37.206 5449-5449/com.shakespace.compose I/System.out: ---- text count = 3 
2021-05-26 11:56:37.206 5449-5449/com.shakespace.compose I/System.out: ---- out count = 3 

外部还是会重绘,但是每次值都会内存缓存中读取,这就是remember的作用,当当前Composable重绘的时候,可以暂存变量值。

如果把变量放在外面?
        setContent {
            println("---- clicked onCreated setContent ")
            var count by mutableStateOf(0) // 移到外面,不适用remember
            Surface() {
                Button(
                    onClick = { count++ }, modifier = Modifier
                        .padding(count.dp)
                        .fillMaxWidth()
                        .height(50.dp)
                ) {
                    Text(
                        text = "I have been clicked $count times",
                        modifier = Modifier.align(Alignment.CenterVertically)
                    )
                    SideEffect(effect = { println("---- text count = $count ") })
                }
                SideEffect(effect = { println("---- out count = $count ") })
            }
        }

数字会增加,控制台输出

2021-05-26 12:03:32.605 9375-9375/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 12:03:32.658 9375-9375/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 12:03:32.658 9375-9375/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 12:03:35.100 9375-9375/com.shakespace.compose I/System.out: ---- text count = 1 
2021-05-26 12:03:35.100 9375-9375/com.shakespace.compose I/System.out: ---- out count = 1 
2021-05-26 12:03:38.646 9375-9375/com.shakespace.compose I/System.out: ---- text count = 2 
2021-05-26 12:03:38.646 9375-9375/com.shakespace.compose I/System.out: ---- out count = 2 
2021-05-26 12:03:39.642 9375-9375/com.shakespace.compose I/System.out: ---- text count = 3 
2021-05-26 12:03:39.642 9375-9375/com.shakespace.compose I/System.out: ---- out count = 3 

这和重绘那个例子是类似的,setContent的参数也是个Composable,Surface作为直接子类,并没有依赖于count,所以本身不会重绘,count也不会被重置。

但是

如果我们把上面的Surface换成Column之类的Composable,其他都不变,结果就不一样了

2021-05-26 12:07:16.163 9780-9780/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 12:07:16.166 9780-9780/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 12:07:16.166 9780-9780/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 12:07:17.047 9780-9780/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 12:07:17.050 9780-9780/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 12:07:17.050 9780-9780/com.shakespace.compose I/System.out: ---- out count = 0 
2021-05-26 12:07:17.883 9780-9780/com.shakespace.compose I/System.out: ---- clicked onCreated setContent 
2021-05-26 12:07:17.885 9780-9780/com.shakespace.compose I/System.out: ---- text count = 0 
2021-05-26 12:07:17.885 9780-9780/com.shakespace.compose I/System.out: ---- out count = 0 

数字不变,而且 “clicked onCreated setContent” 每次都打印,说明setContent里面的Composable每次都会重绘

根据评论区的指正,因为Column Row Box都是inline函数,众所周知,inline运行时代码体会被替换到掉用处,也就是说实际上没有Column这一层,所以上面的例子整个大的函数都会重新执行。