Desarrolladores de Android que han estado usando Compose durante dos años y medio, ¿qué hay de nuevo?

Hola a todos, soy un desarrollador de Android que ha estado usando Compose durante dos años y medio. Hoy les compartiré algo que quieren ver. Ha pasado un tiempo desde el último artículo y es hora de resumir nuevamente.
Durante este período, he estado practicando el uso de Compose para escribir la lógica comercial como se mencionó en el artículo anterior, pero a medida que la lógica comercial y las páginas se vuelven cada vez más complejas, también encontré algunos problemas en el proceso de uso.

Redactar presentador

La lógica empresarial escrita en Compose mencionada en el artículo anterior se escribe así:

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

Las ventajas también se han mencionado en el artículo anterior, por lo que no las repetiré aquí y hablaré de las deficiencias descubiertas durante este período de práctica:

  • Después de que el negocio sea complejo, se dividirán muchos presentadores, lo que hará que la combinación final de presentadores sea muy complicada, especialmente para el procesamiento de acciones de los subpresentadores.
  • Si el Presentador tiene Acción, esta forma de escribir no puede manejar muy bien el regreso anticipado.

decir uno por uno

Procesamiento de acciones combinadas

Cada vez que se llama a un Presentador secundario con Acción, se debe crear al menos un nuevo Canal y un Flujo correspondiente, y se debe agregar un procesamiento de Acción correspondiente, por ejemplo

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

Si la página y la lógica comercial son complejas, la combinación de Presenter generará una gran cantidad de código redundante, que es solo para puente sin ninguna lógica comercial. Y al iniciar la acción del subpresentador en la interfaz de usuario de Compose, también se requiere una llamada de puente, lo que al final puede conducir fácilmente a demasiado código redundante.

Regreso anticipado

Si hay procesamiento de acciones en un presentador, la devolución anticipada debe manejarse con mucho cuidado, por ejemplo:

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

En ese count == 10momento , regresará directamente, omitiendo la subsiguiente suscripción al evento de Acción, lo que hará que los eventos subsiguientes nunca se activen. Por lo tanto, todas las devoluciones deben ser posteriores a la suscripción al evento de acción.

Cuando el negocio es complicado, las dos deficiencias anteriores se convierten en los mayores puntos débiles.

solución

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

Supongo que te gusta

Origin juejin.im/post/7222897518501543991
Recomendado
Clasificación