注册

使用 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 真是天才般的解决思路,本质上并不复杂,但大大简化了声明式

作者:Tlaster
来源:juejin.cn/post/7222897518501543991
UI 的状态管理。

0 个评论

要回复文章请先登录注册