使用 Compose 时长两年半的 Android 开发者,又有什么新总结?
大家好啊,我是使用 Compose 时长两年半的 Android 开发者,今天来点大家想看的东西啊,距离上次文章也已经过去一段时间了,是时候再次总结一下了。
期间一直在实践着之前文章说的使用 Compose 编写业务逻辑,但随着业务逻辑和页面越来越复杂,在使用的过程中也遇到了一些问题。
Compose Presenter
上一篇文章中有提到的用 Compose 写业务逻辑是这样写的:
@Composable
fun Presenter(
action: Flow<Action>,
): State {
var count by remember { mutableStateOf(0) }
action.collectAction {
when (this) {
Action.Increment -> count++
Action.Decrement -> count--
}
}
return State("Clicked $count times")
}
优点在之前的文章中也提到过了,这里就不再赘述,说一下这段时间实践下来发现的缺点:
- 业务复杂后会拆分出非常多的 Presenter,导致在最后组合 Presenter 的时候会非常复杂,特别是对于子 Presenter 的 Action 处理
- 如果 Presenter 有 Action,这样的写法并不能很好的处理 early return。
一个一个说
组合 Action 处理
每调用一个带 Action 的子 Presenter,就至少需要新建一个 Channel 以及对应的 Flow,并且需要增加一个对应的 Action 处理,举个例子
@Composable
fun FooPresenter(
action: Flow<FooAction>
): FooState {
// ...
// 创建子 Presenter 需要的 Channel 和 Flow
val channel = remember { Channel<Action>(Channel.UNLIMITED) }
val flow = remember { channel.consumeAsFlow() }
val state = Presenter(flow)
LaunchedEffect(Unit) {
action.collect {
when (it){
// 处理并传递 Action 到子 Presenter中
is FooAction.Bar -> channel.trySend(it.action)
}
}
}
// ...
return FooState(
state = state,
// ...
)
}
如果页面和业务逻辑复杂之后,组合 Presenter 会带来非常多的冗余代码,这些代码只是做桥接,没有任何的业务逻辑。并且在 Compose UI 中发起子 Presenter 的 Action 时也需要桥接调用,最后很容易导致冗余代码过多。
Early return
如果一个 Presenter 中有 Action 处理,那么需要非常小心的处理 early return,例如:
@Composable
fun Presenter(
action: Flow<Action>,
): State {
var count by remember { mutableStateOf(0) }
if (count == 10) {
return State("Woohoo")
}
action.collectAction {
when (this) {
Action.Increment -> count++
Action.Decrement -> count--
}
}
return State("Clicked $count times")
}
当 count == 10
时会直接 return,跳过后面的 Action 事件订阅,造成后续的事件永远无法触发。所以所有的 return 必须在 Action 事件订阅之后。
当业务复杂之后,上面两个缺点就成为了最大的痛点。
解决方案
有一天半夜我看到了 Slack 的 Circuit 是这样写的:
object CounterScreen : Screen {
data class CounterState(
val count: Int,
val eventSink: (CounterEvent) -> Unit,
) : CircuitUiState
sealed interface CounterEvent : CircuitUiEvent {
object Increment : CounterEvent
object Decrement : CounterEvent
}
}
@Composable
fun CounterPresenter(): CounterState {
var count by rememberSaveable { mutableStateOf(0) }
return CounterState(count) { event ->
when (event) {
is CounterEvent.Increment -> count++
is CounterEvent.Decrement -> count--
}
}
}
这 Action 原来还可以在 State 里面以 Callback 的形式处理,瞬间两眼放光,一次性解决了两个痛点:
- 子 Presenter 不再需要 Action Flow 作为参数,事件处理直接在 State Callback 里面完成,减少了大量的冗余代码
- 在 return 的时候就附带 Action 处理,early return 不再是问题。
好了,之后的 Presenter 就这么写了。期待再过半年的我能再总结出来一些坑吧。
为什么 Early return 会导致事件订阅失效
可能有人会好奇这一点,Presenter 内不是已经订阅过了吗,怎么还会失效。
我们还是从 Compose 的原理开始说起吧。
先免责声明一下:以下是我对 Compose 实现原理的理解,难免会有错误的地方。
网上讲述 Compose 原理的文章都非常多了,这里就不再赘述,核心思想是:Compose 的状态由一个 SlotTable 维护。
还是结合 Early return 的例子来说,我稍微画了一下 SlotTable 在不同时候的状态:
@Composable
fun Presenter(
action: Flow<Action>, count != 10 | count == 10
): State {
var count by remember { mutableStateOf(0) } | State | State |
if (count == 10) { | State | State |
return State("Woohoo") | Empty | State |
} | | |
action.collectAction { | State | Empty |
when (this) { | State | Empty |
Action.Increment -> count++ | State | Empty |
Action.Decrement -> count-- | State | Empty |
} | | |
} | | |
return State("Clicked $count times") | State | Empty |
}
当 count != 10
的时候,SlotTable 内部保存的状态是包含 Action 事件订阅的,但是当 count == 10
之后,SlotTable 就会清空所有之后语句对应的状态,而之后正好包含了 Action 事件订阅,所以订阅就失效了。
我觉得这是 Compose 和 React Hooks 又一个非常相似的地方,React Hooks 的状态也是由一个列表维护的。
再举一个例子:
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column {
var boolean by remember {
mutableStateOf(true)
}
Text(
text = "Hello $name!",
modifier = modifier
)
Button(onClick = {
boolean = !boolean
}) {
Text(text = "Hide counter")
}
if (boolean) {
var a by remember {
mutableStateOf(0)
}
Button(onClick = {
a++
}) {
Text(text = "Add")
}
Text(text = "a = $a")
}
}
}
这段代码大家也可以试试。当我做如下操作时:
- 点击 Add 按钮,此时显示
a = 1
- 点击 Hide counter 按钮,此时 counter 被隐藏
- 再次点击 Hide counter 按钮,此时 counter 显示,其中
a = 0
因为当 counter 被隐藏时,包括变量 a
在内所有的状态都从 SlotTable 里面清除了,那么新出现的变量 a
其实是完全一个新初始化的一个变量,和之前的变量没有任何关系。
总结
过了大半年,也算是对 Compose 内部实现原理又有了一个非常深刻的认识,特别是当我用 C# 自己实现一遍声明式 UI 之后,然后再次感叹:SlotTable 真是天才般的解决思路,本质上并不复杂,但大大简化了声明式
来源:juejin.cn/post/7222897518501543991