Compose 编程思想

本贴最后更新于 229 天前,其中的信息可能已经时移世改

image

一、声明式编程

1. 概念

知乎:什么是声明式编程

稀土掘金:过程式、函数式、命令式、声明式编程模式不同点

我的理解:图灵机 vs lambda 演算

2. 编程方式对比

/**
 * 传统编程方式👎
 */
val button:Button = findViewById(R.id.button)
button.text = "确认"

/**
 * compose 声明式编程👍
 */
@Composable
fun Greeting(name: String, isShowName: Boolean) {
		val showName = if (isShowName) "显示名字" else "不显示"
		Text(text = "Hello $name!  $showName")
}

二、组合和重组

1. 可组合函数

  1. 可组合函数是带有 @Composable 注解的常规函数。
  2. 这类函数自身可以调用其他 @Composable 函数。

2. 组合

组合用于描述界面,通过运行可组合项来生成,也是树的结构。

那么,组合(界面)如何进行更新呢?答案:重组

3. 重组

重组就是系统根据需要使用新数据重新绘制的函数来重新组合,而 Compose 可以智能地仅重组已更改的组件。重组是乐观的操作,可能会被取消。具体在只能重组中解释。

4. 智能重组

4.1 控件在任何顺序执行

如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择并识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。

每个可组合函数都需要保持独立,不能依赖于别的可组合函数。

4.2 控件并行执行

Compose 可以通过并行运行可组合函数来优化重组。这样一来,Compose 就可以利用多个核心,并以较低的优先级运行可组合函数了(不在屏幕上)。

  1. 这种优化意味着,可组合函数可能会在后台线程池中执行。如果某个可组合函数对 ViewModel 调用一个函数,则 Compose 可能会同时从多个线程调用该函数。为了确保应用程序正常运行,所有可组合函数都不应有附带效应,而应通过始终在界面线程上执行的 onClick 等回调触发附带效应。
  2. 调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免使用修改可组合 lambda 中变量的代码,既因为此类代码并非线程安全代码,又因为它是可组合 lambda 不允许的附带效应。
4.3 重组会跳过尽可能多的内容

如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。

这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行在界面树上面或下面的任何可组合项。

同样,执行所有可组合函数或 lambda 都应该没有附带效应。当需要附带效应时,应通过回调触发。

4.4 重组是乐观的操作

只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。取消重组后,Compose 会从重组中舍弃界面树。

  1. 如果需要执行成本高昂的操作(例如从网络或数据库来读取数据),尽量在后台协程中执行,并将值结果作为参数传递给可组合函数。
  2. 如有任何附带效应依赖于显示的界面,则即使取消了重组操作,也会应用该附带效应。这可能会导致应用状态不一致。所以我们应该确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。

5. 总结

5.1 Compose 的工作流程

在初始组合期间,Compose 跟踪为描述界面而调用的可组合项;当应用程序的状态发生变化时,Compose 会安排重组(上一节介绍过重组,这里不再赘述);重组过程中会运行可能已更改的可组合项以响应状态变化,然后 Compose 会更新组合以反映所有更改。这就是 Compose 的工作流程。

5.2 如何创建和更新组合

组合只能通过初始组合生成且只能通过重组进行更新。更新组合的唯一方式是重组。

5.3 如何构建组合以支持重组

在每种情况下,最佳做法都是使可组合函数保持快速、幂等且没有附带效应。

三、Compose 状态

使用声明的方式开发界面的话,初始声明的组合只描述了初始时刻界面的状态,但是界面状态是会发生变化的,此时便需要引入本地状态来保存某一刻界面的状态。

1. Compose 的状态是什么?

应用程序中的状态是指可以随时间变化的任何值。

  1. 这个定义非常宽泛,比如网络中获取的值、数据库中的值,甚至是类中的变量都属于状态。
  2. 之前 Android 中常用的 MVP 架构在封装过程中经常会封装 LCE(Loading、Content、Error),旨在向用户展示应用程序的不同状态。

2. Compose 状态如何实现?

可组合函数可以使用 remember 可组合项记住单个对象。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。使用 val index = remember { mutableStateOf(0) } 传入默认值。

  1. 这样一来,每当 index 的状态改变,便会触发组合进行重组,界面才会发生变化。
  2. remember 可以存储可变对象和不可变对象。mutableStateOf 会创建 MutableState,MutableState 是 Compose 中的可观察类型。remember 可以在重组后保持状态。如果在未使用 remember 的情况下使用 mutableStateOf,每次重组可组合项的时候,系统都会将状态重新初始化为默认值。
  3. 在 MutableState 的值有任何更改的情况下,系统会安排重组读取此值的所有可组合函数,以实现重组。
  4. remember 不会在配置更改后保持状态,比如旋转屏幕或者来电之后系统就会将状态重新初始化为默认值。所以这时使用 remember 就不行了,而需要使用 rememberSaveable。rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,可以经过序列化之后进行保存。

3. 状态提升

如果某个可组合项保持自己的状态(例如下方代码块),就会变得难以重复使用和测试,同时该可组合项与其状态的存储方式也会紧密关联。应该将此可组合项改为无状态可组合项,即不保持任何状态的可组合项。

@Composable
fun TestState3() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        val index = rememberSaveable { mutableStateOf(0) }
        Button(onClick = {
            index.value++
            Log.e("ZHUJIANG123", "TestState: ${index.value}")
        }) {
            Text("Add")
        }  
        Text("${index.value}", fontSize = 30.sp)
    }
}

为此,可以使用状态提升。(例如下方代码块)

状态提升是一种编程模式,在该模式下,可以将可组合项的状态移至该可组合项的调用方。一种简单的方式是使用参数替换状态,同时使用 lambda 表示事件。

@Composable
fun TestState4(index: Int, onIndexChange: (Int) -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(onClick = {
            onIndexChange(index + 1)
        }) {
            Text("Add")
        }
        Text("$index", fontSize = 30.sp)
    }
}

@Composable
fun TestState4() {
    val index = rememberSaveable { mutableStateOf(0) }
    TestState4(index.value) { index.value = it }
}

本质:状态提升就是方法的重载,方便调用而已。

4. ViewModel 和状态

在 Compose 中,可以使用 ViewModel 公开可观察存储器(如 LiveData、Flow、RxJava 等)中的状态,还可以使用它处理影响相应状态的事件。

4.1.1 使用 MutableState 存储状态
class TestViewModel : ViewModel() {

    private val _index = MutableLiveData(0)
    val index: LiveData<Int> = _index

    fun onIndexChange(newName: Int) {
        _index.value = newName
    }
}

@Composable
fun TestState5(testViewModel: TestViewModel = viewModel()) {
    val index by testViewModel.index.observeAsState(0) // 语法糖,很甜的
    TestState4(index) { testViewModel.onIndexChange(it) }
}

@Composable
fun TestState4(index: Int, onIndexChange: (Int) -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(onClick = {
            onIndexChange(index + 1)
        }) {
            Text("Add")
        }
        Text("$index", fontSize = 30.sp)
    }
}
  1. observeAsState 可观察 LiveData<T> 并返回 State<T> 对象,每当 LiveData 发生变化时,该对象都会更新。State<T> 是 Compose 可以直接使用的可观察类型,前面提到的 MutableState 就是可变的 State
  2. 只有 LiveData 在组合中时,observeAsState 才会观察它。
//隐式转换,语法糖,使用属性委托语法(by)隐式地将`State<T>`视为Compose中类型 `T `的对象。
val index by testViewModel.index.observeAsState(0) 
//显式转换,返回 State<T>,在使用的时候就需要通过index.value来获取值了
val index :State<Int> = testViewModel.index.observeAsState(0)
4.1.2 使用其他类型的状态

Compose 并不要求我们必须使用 MutableState<T> 存储状态,它支持其他可观察类型,但是读取其他可观察类型之前,必须将其转换为 State<T>,以便 Compose 可以在可组合项状态发生变化时自动重组界面。

上面已经介绍了 LiveDate 如何转成 State。

//flow
val value: Int by flow.collectAsState(0)
//rxjava2(Compose为RxJava2提供了5个转换方法)
val completed by completable.subscribeAsState() // RxJava2
val value: String by flowable.subscribeAsState("initial")
val value: String by maybe.subscribeAsState("initial")
val value: String by observable.subscribeAsState("initial")
val value: String by single.subscribeAsState("initial")

四、Compose 生命周期

可组合项的生命周期

附带效应

重启效应

1. 可组合项的生命周期

  1. 进入组合
  2. 执行 0 次或多次重组
  3. 退出组合

每次调用时,可组合项在组合中都有自己的生命周期。

  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    333 引用 • 323 回帖 • 66 关注
  • Copmpose
    1 引用
1 操作
jwangzzz 在 2023-09-12 09:32:47 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...