系列第五篇,进入 Compose 中有关 State 状态的学习。
前面几篇笔记讲了那么多内容,都是基于静态界面的展示来说的,即给我一个不变的数据,然后将它展示出来。如何在 Compose 中构建一个随数据而变化的动态界面呢?相信看完这篇就知道了。
1、基本知识
众所周知,Compose 彻底舍弃了 xml 文件,我们需要像 Flutter 一样完全用代码去进行界面的编码,这样做很容易会导致一个问题:界面和数据处理逻辑耦合,从而导致 Activity 中代码臃肿且维护性下降。
虽然提出了许多架构思想,如 MVC、MVP、MVVM 等,一定程度上解耦了界面与数据处理逻辑,但是架构本身就具有一定的复杂性,且对于后续维护成本也相对较高,所以 Compose 一开始就将界面与数据分开来,分别称之为 组合 和 State 状态。
State 状态:官方文档上说 State 状态是指可以随时间变化的任何值。例如,它可能是存储在 Room 数据库中的值、类的变量,加速度计的当前读数等。 怎么理解这个概念呢?我觉得可以简单理解为:我们要展示给用户看的数据。例如,一个商品的展示页面,其实就是根据数据的不同来展示不同的状态,数据正常、数据错误、空数据等不同的数据就是代表了不同的 State 状态。
组合:按照文档上的意思我觉得可以理解为展示给用户的界面,是由多个组合项(Composable组件)组成。
Event事件:指的是从应用外部生成的输入,用于通知程序的某部分发生了变化。如用户的点击,滑动等操作。所以在 Compose 中,Event 事件一般就是引起 State 状态改变的原因。
2、状态的表示
其实可以换一种说法:Compose 中数据的存储和更新如何处理?目前来看的话,可以用 LiveData、StateFlow、Flow、Observable 等表示。可以看出,这些都是一种可观察数据变化的容器,被它们修饰的对象,我们都可以观察到该对象的变化,从而更新界面。没错,都是使用的观察者模式。
在 Compose 的文档中,ViewModel 被推荐为 State状态的管理对象,从而实现将数据与界面展示的 Activity 分离解耦的目的。
2.1 ViewModel
ViewModel 也是 Jetpack 工具库的成员之一,主要用来存储 UI 展示所需要的数据,谷歌推荐的做法是将 Activity 中的数据都放到 ViewModel 里,而且在 Activity、Fragment 重建时 ViewModel 中的数据是不受影响的。还可以通过 ViewModel 来进行 Activity 与 Fragment 之间,或者 Fragment 与 Fragment 之间的通信。
ViewModel 经常与 LiveData 一起使用,但在 Compose 中,推荐使用 MutableState 来具体存储数据的值。
2.2 MutableState<T>
MutableState<T> 是 Compose 中内置的专门用于存储 State 状态的容器,与 LiveData 一样,它可以观察到存储的值的变化。如果项目不是纯 Compose 代码,建议还是用 LiveData,因为 LiveData 是通用的,而 MutableState<T> 是与 Compose 集成了,所以在 Compose 中使用 MutableState 比 LiveData 更简单。
从这里也可看出,Compose 是推荐将 State 状态设置为可观察的,这样当状态发生更改时,Compose 可以自动重组更新界面。
实际上 MutableState<T> 是个接口:
// code 1 interface MutableState<T>: State<T> { override var value: T }
对 value 进行的任何更改都会自动重组用于读取此状态的所有 Composable 函数,也就是说,value 值改变了之后,所有引用了 value 的 Composable 函数都会重新绘制更新。
3、一个简单例子
先来看看效果:
其中有两个控件,一个是 Text,用于显示输入的内容;另一个是 TextField,相当于 View 体系中的 EditText。可以看出,Text 显示的内容可以随着下面的 TextField 中输入的内容实时更新。
如果是在 View 体系中,一般实现的方法是在 EditText 添加一个 TextWatcher 类用于监听输入事件,然后在 onTextChanged 方法中对 TextView 设置输入的内容即可。
再来看一下 Compose 是如何实现这一小功能的 。根据官方推荐,得先有一个 ViewModel 进行状态数据的管理:
// code 2 class ZhuViewModel: ViewModel() { // 状态数据初始化,初始化为空字符串 var inputStr = mutableStateOf("") // 状态更新方法,将新输入的内容赋值给 MutableState<T> 对象的 value 值 fun onInputChange(inputContent: String) { inputStr.value = inputContent } }
可以看出,ViewModel 中需要对状态进行初始化,并且提供相应的更新方法。同时 ViewModel 中不会出现任何与界面相关的对象,例如 Activity、Fragment、Context 等,为的就是解耦。
界面代码就是 Composable 函数根据 ViewModel 管理的 State 状态进行展示:
// code 3 class ZhuStateActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val zhuViewModel by viewModels<ZhuViewModel>() setContent {InputShow(zhuViewModel)} } } @Composable fun InputShow(viewModel: ZhuViewModel) { Column(Modifier.padding(20.dp)) { Text(text = viewModel.inputStr.value) TextField( value = viewModel.inputStr.value, onValueChange = { viewModel.onInputChange(it) } ) } }
TextField 组件相当于 EditText,onValueChange 可获取到用户的输入内容,在这里调用 ViewModel 中更新状态的方法。这样,所有引用了 ViewModel 中 MutableState 类型对象 inputStr 的组合项(Composable 函数),都会自动重绘更新,Text 组件就可以实时更新输入的内容了。
4. remember 关键字
其实在 code 3 中的小功能使用 ViewModel 来管理 State 状态有点小题大做了,可以用 remember 关键字来实现。这个关键字的作用如它的意思一样,“记住” 它所修饰的对象的值。下面的代码就是没有使用 ViewModel 的实现方法:
// code 4 class ZhuStateActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent {InputShow()} } } @SuppressLint("UnrememberedMutableState") @Composable fun InputShow() { val inputStr = mutableStateOf("Hello") Column(Modifier.padding(20.dp)) { Text(text = inputStr.value) TextField( value = inputStr.value, onValueChange = { inputStr.value = it } ) } }
这里没有使用 remember 会有红线提醒,我先使用 SuppressLint 去掉了报错,为的只是举个栗子,并且设置了默认展示 “Hello” 文案。运行一下,你会发现,不管输入什么,都只是展示 “Hello”,好像啥也没有发生。。。
这是为啥?加一些 log 看看:
// code 5 @SuppressLint("UnrememberedMutableState") @Composable fun InputShow() { val inputStr = mutableStateOf("Hello") Log.d(TAG, "InputShow: Column inputStr = ${inputStr.value}") Column(Modifier.padding(20.dp)) { Text(text = inputStr.value) TextField( value = inputStr.value, onValueChange = { inputStr.value = it Log.d(TAG, "InputShow: onValueChange inputStr = $it") } ) } }
连续输入字母 w、o、r、l、d,打出来的 log 是这样的:
可见在每次输入之后,都会触发 Composable 函数重新绘制,每次都会重新初始化 inputStr 这个状态,而初始值都是一样的,所以看起来就好像输入不起作用。Composable 函数的重新绘制过程也被称之为 重组。
重组:使用新的输入Event事件重新调用可组合项以更新 Compose 树的过程。这一过程会再次运行相同的 Composable 组件进行更新。
顺带说一下,Compose 首次运行渲染 Composable 组件时,会为所有被调用的 Composable 组件构建一个树,然后在重组期间会使用新的 Composable 组件去更新树。
再回到这个例子,使用 remember 关键字就可以避免每次重组时都初始化为初始值。使用后的代码为:
// code 6 @Composable fun InputShow() { val inputStr = remember{ mutableStateOf("Hello") } Column(Modifier.padding(20.dp)) { Text(text = inputStr.value) TextField( value = inputStr.value, onValueChange = { inputStr.value = it } ) } }
这样就可以正确实现功能了。其实 remember 关键字的使用是由两部分组成:
- key arguments:表示这次调用使用的 “键”(key),用圆括号包裹;
- calculation :一个 Lambda 表达式,计算得出需要存储的 “值”(value)。
所以,remember 的用法如下所示:
// code 7 remember(key) { calculation: () -> T }
remember 关键字可以为 Composable 组件项提供一个数据存储空间,系统会将由 calculation Lambda 表达式计算得出的值存储到组合树中,只有当 remember 的 “键” 发生变化时,才会重新执行 calculation 计算得出 value 并存储起来;否则还是原来的值。
当然 code 6 中并没有设置 remember 的 key,这种情况下,remember 会默认该 key 没有发生变化,不会重新初始化,而是用之前的值。
需要注意的点: remember 虽然会将数据或对象存储在组合项中,但当调用 remember 的可组合项从组合树中移除后,它会忘记该数据或对象。所以,不要在有添加或移除 Composable 组件的情况下,使用 remember 将重要内容存储在 Composable 组件中,因为添加和移除都会使得数据丢失。