Android developers who have been using Compose for two and a half years, what's new?

Hello everyone, I am an Android developer who has been using Compose for two and a half years. Today, I will share something you want to see. It has been a while since the last article, and it is time to summarize again.
During this period, I have been practicing the use of Compose to write business logic as mentioned in the previous article, but as the business logic and pages become more and more complex, I also encountered some problems in the process of using it.

Compose Presenter

The business logic written in Compose mentioned in the previous article is written like this:

@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")
}
复制代码

The advantages have also been mentioned in the previous article, so I won’t repeat them here, and talk about the shortcomings discovered during this period of practice:

  • After the business is complex, a lot of Presenters will be split, which will make the final combination of Presenters very complicated, especially for the Action processing of sub-Presenters
  • If the Presenter has Action, this way of writing can't handle early return very well.

say one by one

Combination Action processing

Every time a child Presenter with Action is called, at least a new Channel and a corresponding Flow need to be created, and a corresponding Action processing needs to be added, for example

@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,
        // ...
    )
}
复制代码

If the page and business logic are complex, combining the Presenter will bring a lot of redundant code, which is only for bridging without any business logic. And when initiating the sub-Presenter's Action in the Compose UI, a bridge call is also required, which can easily lead to too much redundant code in the end.

Early return

If there is Action processing in a Presenter, the early return needs to be handled very carefully, for example:

@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")
}
复制代码

At that count == 10time , it will return directly, skipping the subsequent Action event subscription, causing subsequent events to never be triggered. So all return must be after Action event subscription.

When the business is complicated, the above two shortcomings become the biggest pain points.

solution

有一天半夜我看到了 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 的状态管理。

おすすめ

転載: juejin.im/post/7222897518501543991