1、自定义 Composable
为什么所有组件都要加 @Composable 注解才可以使用?
这是因为 Compose 需要通过 Compose 的编译器插件(Compose Compiler Plugin)在组件函数中增加一些参数,这些参数在调用时有用。通过编译器增加这些参数,既可以在调用时可以被程序正确使用,又不用使用者手动添加(省事了),一举两得。
这实际上是一种面向切面编程(AOP)的实例,通常 AOP 会借助以下两者之一:
- Annotation Processor
- 修改字节码(字节码插桩)
Compose 没有使用以上两种技术方案,主要是因为 Compose 要跨平台,而上述两种技术方案都是针对 JVM 的,在其他平台上无效。其次,编译器插件的功能要比 Annotation Processor 更强大。
说回来,那么 Compose 编译器插件是如何知道应该修改哪些函数的呢?就是通过 @Composable 注解,该注解起到了一个识别符的作用。
总结一下,由于 Compose 编译器插件要对组件函数进行修改,注解可以帮助编译器正确识别那些需要被修改的函数。
被 @Composable 修饰的函数,只能在同样被 @Composable 修饰的函数中调用,这样到了最上层的 setContent,我们能看到,该函数虽然自身没有被 @Composable 修饰,但是其将闭包内容封装为 @Composable 函数进行调用也是可以的:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
...
}
在执行这个根部的 @Composable 函数时,实际上是将其强转为一个 Function2 类型的函数 invokeComposable(),然后直接调用(后续讲 setContent 函数时会详解)。
@Composable 函数只能在 @Composable 函数中被调用,这一点与挂起函数只能在协程或其他挂起函数中调用的规则很相似。前者是通过 Composer 编译器插件实现的,后者是 Kotlin 编译器实现的。
加了 @Composable 注解的函数会被简称为 Composable 或 Composable 函数。
2、MutableState 和 mutableStateOf()
Compose 对 UI 进行一次声明,后续当数据发生变化时,会自动更新到 UI 上,而无需像传统 View 体系那样,以命令式的方式让 UI 更新数据。
上述描述隐含着一个条件,就是只有可以被监听的数据,在其发生变化时,监听者才能拿到变化后的数据去更新 UI。所以数据不能是普通的变量,而需要用 mutableStateOf() 来创建一个 MutableState:
val name = mutableStateOf("James")
通过 value 获取属性值:
Text(name.value)
接下来探查自动订阅是如何形成的。
mutableStateOf() 的返回值类型是 MutableState 这个接口类型:
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
而提供 MutableState 的具体类型的是 createSnapshotMutableState():
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
createSnapshotMutableState() 返回的是 MutableState 的子接口 SnapshotMutableState 的实现类的子类对象 ParcelableSnapshotMutableState:
internal actual fun <T> createSnapshotMutableState(
value: T,
policy: SnapshotMutationPolicy<T>
): SnapshotMutableState<T> = ParcelableSnapshotMutableState(value, policy)
ParcelableSnapshotMutableState 继承了 SnapshotMutableStateImpl 也实现了 Parcelable,对 MutableState 内 value 属性的实现就来自于 SnapshotMutableStateImpl:
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
}
后续关注 value 的 getter 和 setter。
Compose 的界面刷新包括组合(Composition)、布局、绘制三个过程。其中组合是指利用 Composable 函数拼凑出页面实际内容(各个组件的 LayoutNode)的过程。Composable 函数本身并不是界面的元素,而是用于生成界面元素的。
2.1 getter
MutableState -> StateObject -> StateRecord -> Compose 支持事务功能(简单理解为可以批量进行、可以撤销、并发进行并且可以进行事后合并)
事务功能包含撤销,撤销需要保持旧值,因此 Compose 的变量管理需要对同一个变量保存多个新旧值。
Compose 使用链表来保存 StateRecord,只需在 StateRecord 中保存 StateRecord 链表的头节点 firstStateRecord 即可获取整个链表。
分析 value 的 get() 都做了哪些事:
override var value: T
// 订阅,取值返回
get() = next.readable(this).value
首先,这个 next 的类型是 StateStateRecord,该类型是 StateRecord 的子类:
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
private var next: StateStateRecord<T> = StateStateRecord(value)
// 链表头的取值为 next
override val firstStateRecord: StateRecord
get() = next
// StateStateRecord 是 StateRecord 的子类
private class StateStateRecord<T>(myValue: T) : StateRecord() {
override fun assign(value: StateRecord) {
@Suppress("UNCHECKED_CAST")
this.value = (value as StateStateRecord<T>).value
}
override fun create(): StateRecord = StateStateRecord(value)
var value: T = myValue
}
}
在 next 上调用的 readable() 是以 StateRecord 为上限的类型的扩展函数,虽然最终返回的就是该扩展类型的对象,但是在返回前做了一些事:
/**
* 返回当前快照的当前可读状态记录。假定[this]是[state]的第一个记录。
*/
fun <T : StateRecord> T.readable(state: StateObject): T {
val snapshot = Snapshot.current
// 这行代码的逻辑很深,暂且不深入追溯,先只说说它做了什么。当外界访问 value 的值时会调用其 get()
// 进而执行到 readable(),此时要记录(实际上是订阅)下这个 value 所属的 state 被访问/使用过。
// 这些被访问过的 state 被标记为失效,在下一帧画面要刷新时会进行重组(Recompose)。
snapshot.readObserver?.invoke(state)
// 三参数 readable() 会拿到可用的中最新的 StateRecord
return readable(this, snapshot.id, snapshot.invalid) ?: sync {
// 当全局快照已被另一个线程推进,并且在该线程暂停期间写入对象的状态被覆盖时,可读状态可能
// 返回 null。在这里重复读取是有效的,因为要么这将返回与上一次调用相同的结果,要么将找到
// 一个有效记录。在 sync 块中阻止其他线程写入该状态对象,直到读取完成。
val syncSnapshot = Snapshot.current
readable(this, syncSnapshot.id, syncSnapshot.invalid)
} ?: readError()
}
在这个过程中,需要注意的一件事是:mutableStateOf() 得到一个 ParcelableSnapshotMutableState 对象,该对象继承了 SnapshotMutableStateImpl,这个实现类实现了 SnapshotMutableState 接口:
/**
* 一个可变的值持有者,在[Composable]函数执行期间对[value]属性的读取,当前的[RecomposeScope]将订阅
* 该值的更改。当[value]属性被写入并更改时,任何订阅的[RecomposeScope]将被安排重新组合。对其的写入作为
* [Snapshot]系统的一部分进行事务处理。
*/
interface SnapshotMutableState<T> : MutableState<T> {
/**
* 控制在可变快照中如何处理更改的策略。
*/
val policy: SnapshotMutationPolicy<T>
}
也实现了 StateObject 接口:
/**
* 所有快照感知状态对象都实现的接口。被该模块用于维护状态对象的状态记录。
*/
@JvmDefaultWithCompatibility
interface StateObject {
/**
* 状态记录链表中的第一个状态记录。
*/
val firstStateRecord: StateRecord
/**
* 在列表的开头添加一个新的状态记录。调用此方法后,[firstStateRecord]应为[value]。
*/
fun prependStateRecord(value: StateRecord)
/**
* 基于冲突的状态更改生成合并的状态。
*
* 该方法不得修改接收到的任何记录,并应将状态记录视为不可变的,即使是[applied]记录也是如此。
*
* @param previous 用于创建[applied]记录的状态记录,也是产生[current]记录的状态(虽然间接)。
*
* @param current 父快照或全局状态的状态记录。
*
* @param applied 正在应用的父快照或全局状态的状态记录。
*
* @return 修改后的状态,如果值无法合并则为null。如果无法合并状态,则当前应用将失败。
* 任何参数都可以作为结果返回。
* 如果不是参数值之一,则必须通过调用传递的记录之一上的[StateRecord.create]来创建一个新值,
* 然后可以修改为合并值后返回。
* 如果返回一个新记录,[MutableSnapshot.apply]将更新内部快照ID并调用
* [prependStateRecord](如果使用了该记录)。
*/
fun mergeRecords(
previous: StateRecord,
current: StateRecord,
applied: StateRecord
): StateRecord? = null
}
ParcelableSnapshotMutableState 可以被订阅的能力不是来自于 SnapshotMutableState,而是 StateObject。不要被类的名字所迷惑,看起来似乎是由 SnapshotMutableState 提供订阅功能的。但是你看 SnapshotMutableState 及其父接口 MutableState 都没有提供这种能力。
总结:为什么 mutableStateOf() 返回的对象可以被订阅?因为它内部的 value 属性的 get 函数被定制为每次取值时先进行记录操作,记录下这个值在哪里被取用了,然后从保存的众多值当中取到最新的可用值返回。
2.2 setter
再看 setter 的实现:
override var value: T
set(value) = next.withCurrent { // it: StateStateRecord<T>
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
next 已经知道什么了,直接看它的 withCurrent(),该函数会直接调用它参数上的 block 函数:
// block 函数的参数类型与 withCurrent() 的接收者类型相同
inline fun <T : StateRecord, R> T.withCurrent(block: (r: T) -> R): R =
block(current(this))
其中 current() 会取到一个最新的 StateRecord:
@PublishedApi
internal fun <T : StateRecord> current(r: T) =
Snapshot.current.let { snapshot ->
// 三个参数的 readable(),只取值,不订阅
readable(r, snapshot.id, snapshot.invalid) ?: sync {
Snapshot.current.let { syncSnapshot ->
readable(r, syncSnapshot.id, syncSnapshot.invalid)
}
} ?: readError()
}
因此 set() 的逻辑就是先判断新旧值是否相等,如果不等再调用 overwritable():
internal inline fun <T : StateRecord, R> T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R
): R {
var snapshot: Snapshot = snapshotInitializer
return sync {
snapshot = Snapshot.current
this.overwritableRecord(state, snapshot, candidate).block()
}.also {
notifyWrite(snapshot, state)
}
}
这一步主要看 overwritableRecord():
internal fun <T : StateRecord> T.overwritableRecord(
state: StateObject,
snapshot: Snapshot,
candidate: T
): T {
if (snapshot.readOnly) {
// If the snapshot is read-only, use the snapshot recordModified to report it.
snapshot.recordModified(state)
}
val id = snapshot.id
// 1.如果 candidate 的 snapshotId 与 snapshot 的 id 匹配,就返回 candidate
if (candidate.snapshotId == id) return candidate
// 2.如果上一步没返回,就生成一个新的 StateRecord 类型的数据并返回
val newData = newOverwritableRecord(state)
newData.snapshotId = id
snapshot.recordModified(state)
return newData
}
StateRecord 每次修改前后的新旧值都会被存起来串成一个链表,链表上的各个节点都对应了某一时刻 Compose 的整个内部状态。Compose 记录每个变量的每个状态,用的是 StateRecord 的链表。而具体各个链表上的哪些节点属于同一个状态,它也有记录,这个记录就是 SnapShot。
SnapShot 的功能就是对 Compose 内的各个变量做快照。SnapShot 记录整个系统的状态,可以对应多个 StateRecord;而一个 StateRecord 只对应一个 SnapShot。
有了快照功能后,就可以在某些变量值发生变化的时候,不必马上将这个变化应用到界面上,而是在跑完整个 Compose 的流程之后,把所有发生变化的变量一起应用。直接拿着最终结果去进行接下来的布局和绘制,这样性能会好一些。SnapShot 对这种批量应用改变提供了底层的技术可行性支持。
当然,SnapShot 不止有这一个帮助,它还是 Compose 支持多线程同步对界面进行计算的下层技术支持。
- 系统有多个 SnapShot 时,它们是有先后关系的
- 同一个 StateObject 的每个 StateRecord 都有它们对应的 SnapShot 的 id。StateRecord 和 SnapShot 就算不直接对应,只要 StateRecord 的 SnapShot 对另一个是有效的,另一个就能取到这个 StateRecord
overwritable() 内还有一个 notifyWrite():
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
该函数会去查找这个 state 在哪里被读取了,然后去读取它的位置将状态标记为失效。
get() 与 set() 并没有完成我们所认为的订阅工作,Compose 需要两套订阅机制同时工作才能完成订阅。
两个订阅过程:
- 对 SnapShot 中读写 StateObject 对象的订阅,分别订阅读和写,所以有两个接收者:readObserver 和 writeObserver。发生时间:订阅是在 SnapShot 创建时,通知是在读和写的时候
- 对每一个 StateObject 的应用做订阅。发生时间:订阅发生在第一个订阅的 readObserver 被调用(通知)的时候;通知发生在 StateObject 新值被应用的时候(后面讲)
最后讲的通过 by 替换 = 的用法(好处是不用每次获取与赋值时都调用 value 了),需要手动导入 getValue 和 setValue 这两个扩展函数。
3、Recompose Scope 和 remember()
Recompose Scope 是重组作用域,作用域越小,其内部包含的与重组无关的代码就越少,无用的工作也就做得少。
remember() 可以防止由于 Recompose 导致的预期之外的某些变量的反复初始化,反复初始化可能会带来意外的结果。
何时使用 remember()?这个问题取决于初始化代码是否会被包进 Recompose 的过程。即便在当前所在的组合函数中,能确保初始化过程不会进入重组,但是你不能确保调用这个组合函数的上级组合函数不会带着它一起进入重组,所以在 Compose 中,没有办法判断初始化是否会在重组过程中执行。既然无法判断,那么解决方案也就简单粗暴了,对所有使用了 mutableStateOf() 进行初始化的变量,都套上 remember() 即可。
remember() 起到缓存作用,防止多次初始化,全部包上就对了。
如果初始化在 Composable 函数外面,由于不会重组了,因此就用不到 remember() 了,并且你想写也写不了,因为它也是个 Composable 函数,不能在 Composable 函数之外的环境中调用。
remember() 是可以传数 key 的:
@Composable
inline fun <T> remember(
key1: Any?,
key2: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T
当被 remember() 括起来的内容发生变化时,需要重新计算,但如果不传参数 key,它不会重新计算。因此需要将在括号内参与计算的数据作为 remember() 的 key,这样一旦 key 发生变化,remember() 就会重新计算括号内的数据。
4、无状态、状态提升和单向数据流
4.1 什么是无状态
Compose 官方称其是无状态的(Stateless),这个状态是指组件属性。
比如说 TextView 内保存的文字内容就是一个状态,你可以通过 getText() 与 setText() 获取与设置文字。但在 Compose 中,组件没有状态,也就是其内部不会保存这些数据,在将数据设置到 UI 上之后,它们就被“扔掉了”。
但需要注意的是,无状态作为 Compose 的一个特点,它是允许组件无状态,而不是说组件绝对没有状态。看下面的例子:
@Composable
fun Hello() {
var text = "Hello"
Text(text)
}
text 变量存在于 Hello 函数中,因此 Hello 是有状态的,而 Text 内部没有保存任何变量,因此 Text 是无状态的。
确切地说,Compose 组件是内部无状态,但是状态可以存在于外部。
4.2 状态提升
那么,在 Compose 中,从外部如何获取某个组件的状态?可以分为两种情况:
- 对于无状态组件,由于组件内部无状态,而这个状态是存在于外部的(就好比上面示例中,Text 内部是无状态的,但是它的状态 text 是在外部提供的),因此可以直接在外部获取这个状态
- 对于有状态组件,由于状态在函数内是一个局部变量,从语言角度上说,你无法在一个函数的外部获取到该函数的局部变量,因此你可以效仿无状态组件,将状态提出到函数之外,将有状态组件变成无状态组件,然后像无状态组件那样获取状态
以上面的 Hello 函数为例,它内部的状态 text 可以提出到外部,在外部获取这个状态:
setContent {
var text = "Hello"
Hello(text)
// 获取状态,也可以修改
text = "HaHaHa"
}
// 状态提出去之后,作为参数接收这个状态
@Composable
fun Hello(value: String) {
Text(value)
}
Hello 在提取状态之后,从有状态组件变成了无状态组件。这种将状态从子组件移动到父组件,以便在整个组件层次结构中共享和管理状态的模式叫做状态提升(State Hoisting)。
状态提升有一个原则:尽量不往上提。因为状态提的越高,能访问该状态的范围就越广,代码出错的概率就越高。因此状态要尽可能地往下放。
对于可以互动的组件,除了提取状态之外,还需要提取交互的函数到外部。
4.3 单项数据流
当应用内的数据来源有多个渠道时,如何安排这些数据呢?
比如一个新闻应用,通过网络 + 本地数据库两种方式来展示数据:
- 第一次打开应用时,本地数据库是空的,从网络获取新闻列表,显示到 UI 上并存到数据库中
- 滑动到底部加载下一页数据,取到新的数据后合并到内存后显示到 UI 上,同时也要存入数据库中
- 杀死应用后重新打开,可以从本地数据库加载数据显示,同时从网络获取数据,获取之后合并到内存后显示,仍需要保存到数据库
第三种情况涉及到数据有效性的问题。通常数据库读取数据要比网络请求数据要快很多,因此先读取数据库的数据显示到 UI 上,然后等拿到网络请求的数据之后再显示网络数据。但是也有极端情况,可能网络数据比读取数据库的速度快,那么可能会出现数据库中较老的数据覆盖了网络请求的较新数据。
像这种双数据来源都需要解决这种数据有效性或数据同步性的问题。较为常用的解决方式是采用单数据来源,让多个数据源串行合并为单个数据源。比如这里我们就可以让网络数据源作为本地数据库的上游,即网络数据先存入数据库,数据库再为 UI 提供单一的数据来源,即单一信息源(Single Source of Truth)。
单一信息源是 Compose 官方建议使用的,当然这种模式在 Compose 之前的 Jetpack 开始就已经被 Google 官方推荐了(ViewModel 的 Repository 也有数据库和网络两个数据源,官方也是建议让网络数据存入数据库,数据库再为 UI 提供数据这种方式)。Compose 建议所有界面中会用到的数据都采用这种形式。
单向数据流(Unidirectional Data Flow)怎么用?把 Composable 函数做好封装,做状态提升时提的完整一点。如果有用户交互,在提状态时应该把与之相关的用户交互也往上提,即把用户事件做成函数类型以函数参数的形式暴露出来,把这个用户事件也交给上层来调用。
对于 Compose 而言,实现了单向数据流,也就实现了单一信息源。