一、声明式编程
1. 概念
我的理解:图灵机 vs lambda 演算
2. 编程方式对比
/**
* 传统编程方式👎
*/
val button:Button = findViewById(R.id.button)
button.text = "确认"
/**
* compose 声明式编程👍
*/
@Composable
fun Greeting(name: String, isShowName: Boolean) {
val showName = if (isShowName) "显示名字" else "不显示"
Text(text = "Hello $name! $showName")
}
二、组合和重组
1. 可组合函数
- 可组合函数是带有 @Composable 注解的常规函数。
- 这类函数自身可以调用其他 @Composable 函数。
2. 组合
组合用于描述界面,通过运行可组合项来生成,也是树的结构。
那么,组合(界面)如何进行更新呢?答案:重组
3. 重组
重组就是系统根据需要使用新数据重新绘制的函数来重新组合,而 Compose 可以智能地仅重组已更改的组件。重组是乐观的操作,可能会被取消。具体在只能重组中解释。
4. 智能重组
4.1 控件在任何顺序执行
如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择并识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。
每个可组合函数都需要保持独立,不能依赖于别的可组合函数。
4.2 控件并行执行
Compose 可以通过并行运行可组合函数来优化重组。这样一来,Compose 就可以利用多个核心,并以较低的优先级运行可组合函数了(不在屏幕上)。
- 这种优化意味着,可组合函数可能会在后台线程池中执行。如果某个可组合函数对 ViewModel 调用一个函数,则 Compose 可能会同时从多个线程调用该函数。为了确保应用程序正常运行,所有可组合函数都不应有附带效应,而应通过始终在界面线程上执行的 onClick 等回调触发附带效应。
- 调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免使用修改可组合 lambda 中变量的代码,既因为此类代码并非线程安全代码,又因为它是可组合 lambda 不允许的附带效应。
4.3 重组会跳过尽可能多的内容
如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。
这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行在界面树上面或下面的任何可组合项。
同样,执行所有可组合函数或 lambda 都应该没有附带效应。当需要附带效应时,应通过回调触发。
4.4 重组是乐观的操作
只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。取消重组后,Compose 会从重组中舍弃界面树。
- 如果需要执行成本高昂的操作(例如从网络或数据库来读取数据),尽量在后台协程中执行,并将值结果作为参数传递给可组合函数。
- 如有任何附带效应依赖于显示的界面,则即使取消了重组操作,也会应用该附带效应。这可能会导致应用状态不一致。所以我们应该确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。
5. 总结
5.1 Compose 的工作流程
在初始组合期间,Compose 跟踪为描述界面而调用的可组合项;当应用程序的状态发生变化时,Compose 会安排重组(上一节介绍过重组,这里不再赘述);重组过程中会运行可能已更改的可组合项以响应状态变化,然后 Compose 会更新组合以反映所有更改。这就是 Compose 的工作流程。
5.2 如何创建和更新组合
组合只能通过初始组合生成且只能通过重组进行更新。更新组合的唯一方式是重组。
5.3 如何构建组合以支持重组
在每种情况下,最佳做法都是使可组合函数保持快速、幂等且没有附带效应。
三、Compose 状态
使用声明的方式开发界面的话,初始声明的组合只描述了初始时刻界面的状态,但是界面状态是会发生变化的,此时便需要引入本地状态来保存某一刻界面的状态。
1. Compose 的状态是什么?
应用程序中的状态是指可以随时间变化的任何值。
- 这个定义非常宽泛,比如网络中获取的值、数据库中的值,甚至是类中的变量都属于状态。
- 之前 Android 中常用的 MVP 架构在封装过程中经常会封装 LCE(Loading、Content、Error),旨在向用户展示应用程序的不同状态。
2. Compose 状态如何实现?
可组合函数可以使用 remember 可组合项记住单个对象。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。使用 val index = remember { mutableStateOf(0) }
传入默认值。
- 这样一来,每当 index 的状态改变,便会触发组合进行重组,界面才会发生变化。
- remember 可以存储可变对象和不可变对象。mutableStateOf 会创建 MutableState,MutableState 是 Compose 中的可观察类型。remember 可以在重组后保持状态。如果在未使用 remember 的情况下使用 mutableStateOf,每次重组可组合项的时候,系统都会将状态重新初始化为默认值。
- 在 MutableState 的值有任何更改的情况下,系统会安排重组读取此值的所有可组合函数,以实现重组。
- remember 不会在配置更改后保持状态,比如旋转屏幕或者来电之后系统就会将状态重新初始化为默认值。所以这时使用 remember 就不行了,而需要使用 rememberSaveable。rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,可以经过序列化之后进行保存。
3. 状态提升
如果某个可组合项保持自己的状态(例如下方代码块),就会变得难以重复使用和测试,同时该可组合项与其状态的存储方式也会紧密关联。应该将此可组合项改为无状态可组合项,即不保持任何状态的可组合项。
@Composable
fun TestState3() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val index = rememberSaveable { mutableStateOf(0) }
Button(onClick = {
index.value++
Log.e("ZHUJIANG123", "TestState: ${index.value}")
}) {
Text("Add")
}
Text("${index.value}", fontSize = 30.sp)
}
}
为此,可以使用状态提升。(例如下方代码块)
状态提升是一种编程模式,在该模式下,可以将可组合项的状态移至该可组合项的调用方。一种简单的方式是使用参数替换状态,同时使用 lambda 表示事件。
@Composable
fun TestState4(index: Int, onIndexChange: (Int) -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = {
onIndexChange(index + 1)
}) {
Text("Add")
}
Text("$index", fontSize = 30.sp)
}
}
@Composable
fun TestState4() {
val index = rememberSaveable { mutableStateOf(0) }
TestState4(index.value) { index.value = it }
}
本质:状态提升就是方法的重载,方便调用而已。
4. ViewModel 和状态
在 Compose 中,可以使用 ViewModel 公开可观察存储器(如 LiveData、Flow、RxJava 等)中的状态,还可以使用它处理影响相应状态的事件。
4.1.1 使用 MutableState 存储状态
class TestViewModel : ViewModel() {
private val _index = MutableLiveData(0)
val index: LiveData<Int> = _index
fun onIndexChange(newName: Int) {
_index.value = newName
}
}
@Composable
fun TestState5(testViewModel: TestViewModel = viewModel()) {
val index by testViewModel.index.observeAsState(0) // 语法糖,很甜的
TestState4(index) { testViewModel.onIndexChange(it) }
}
@Composable
fun TestState4(index: Int, onIndexChange: (Int) -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = {
onIndexChange(index + 1)
}) {
Text("Add")
}
Text("$index", fontSize = 30.sp)
}
}
observeAsState
可观察LiveData<T>
并返回State<T>
对象,每当LiveData
发生变化时,该对象都会更新。State<T>
是 Compose 可以直接使用的可观察类型,前面提到的MutableState
就是可变的State
。- 只有 LiveData 在组合中时,
observeAsState
才会观察它。
//隐式转换,语法糖,使用属性委托语法(by)隐式地将`State<T>`视为Compose中类型 `T `的对象。
val index by testViewModel.index.observeAsState(0)
//显式转换,返回 State<T>,在使用的时候就需要通过index.value来获取值了
val index :State<Int> = testViewModel.index.observeAsState(0)
4.1.2 使用其他类型的状态
Compose 并不要求我们必须使用 MutableState<T>
存储状态,它支持其他可观察类型,但是读取其他可观察类型之前,必须将其转换为 State<T>
,以便 Compose 可以在可组合项状态发生变化时自动重组界面。
上面已经介绍了 LiveDate 如何转成 State。
//flow
val value: Int by flow.collectAsState(0)
//rxjava2(Compose为RxJava2提供了5个转换方法)
val completed by completable.subscribeAsState() // RxJava2
val value: String by flowable.subscribeAsState("initial")
val value: String by maybe.subscribeAsState("initial")
val value: String by observable.subscribeAsState("initial")
val value: String by single.subscribeAsState("initial")
四、Compose 生命周期
可组合项的生命周期
附带效应
重启效应
1. 可组合项的生命周期
- 进入组合
- 执行 0 次或多次重组
- 退出组合
每次调用时,可组合项在组合中都有自己的生命周期。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于