我们好啊,我是运用 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 真是天才般的处理思路,本质上并不杂乱,但大大简化了声明式 UI 的状况管理。