Compose 编程思想和重组

参阅:developer.android.com/develop/ui/...

本文是对官方文档的理解和补充说明,多数内容来自于官方文档。

Jetpack Compose 是一个适用于 Android 的全新的声明式 UI 工具包。一切都要从声明式说起。

声明式编程

声明式编程范式

长期以来,Android 视图层次结构一直可以表示为界面 widget 树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。

最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态。

手动操纵视图会提高出错的可能性。如果一条数据在多个位置的 View 上呈现,很容易忘记更新显示它的某个 View。另一方面,当两处 UI 同时修改同一条数据,产生冲突也很容易造成异常情况。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。

在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。

声明式范式转换

在传统的 Android UI 开发工具中,Widget(View) 通过实例化,并构造一个结构树的形式来创建页面。通常可以通过 XML 来实现,每个 Widget 都维护着自己的内部状态,通过对象的形式对外提供 getter 和 setter 方法,从而允许应用的逻辑层与 Widget 进行交互。

在 Compose 的声明性方法中,Widget 没有了内部状态,并且不提供 setter 或 getter 函数。实际上,Widget 不再以对象形式提供,而是通过方法的形式提供,用户可以通过调用带有不同参数的同一个可组合函数来更新 UI。

这样的好处是使得架构分层会更加清晰,状态由逻辑层或数据层提供,可组合函数在每次观察到数据更新时调用,更新界面。

应用逻辑为顶层的可组合函数 Screen 提供数据。该函数通过调用其他可组合函数来使用这些数据描述 UI,将适当的数据传递给这些可组合函数,并沿层次结构向下传递数据,最终传递给结构树中的叶子节点,蓝色的部分。

当用户与界面交互时,界面会发起 onClick 等事件。这些事件应通知应用逻辑,应用逻辑随后可以改变应用的状态。当状态发生变化时,系统会使用新数据再次调用可组合函数。这会导致重新绘制界面元素,此过程称为"重组"。 用户与界面元素进行了交互,这里的体现是最底层叶子节点中的一个(标绿色的节点),导致触发一个事件。应用逻辑响应该事件,然后系统根据需要使用新参数自动再次调用可组合函数。

重组

在传统的 Android UI 开发中,如需更改某个 widget,您可以在该 widget 上调用 setter 以更改其内部状态。

在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 -- 系统会根据需要使用新数据重新绘制函数发出的 widget。因为重组整个界面树在计算上成本高昂,会消耗计算能力并缩短电池续航时间,所以 Compose 框架支持可以智能地仅重组已更改的组件。

重组是指在输入更改时再次调用可组合函数的过程。当函数的输入更改时,会发生这种情况。当 Compose 根据新输入重组时,它仅调用可能已更改的函数或 lambda,而跳过其余函数或 lambda。通过跳过所有未更改参数的函数或 lambda,Compose 可以高效地重组。

可组合函数可以按任何顺序执行

如果您看一下可组合函数的代码,可能会认为这些代码按其出现的顺序运行。但其实未必是这样。如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。

例如,假设您有如下代码,用于在标签页布局中绘制三个屏幕:

kotlin 复制代码
@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

StartScreenMiddleScreenEndScreen 的调用可以按任何顺序进行。这意味着,举例来说,您不能让 StartScreen() 设置某个全局变量并让 MiddleScreen() 利用这项更改。相反,其中每个函数都需要保持独立。

附带效应

所谓的附带效应,指的是利用在 Compose 可组合函数重组过程中产生的变化,触发其他逻辑。

例如,在一个可组合函数中更新了某个全局变量,而另一个可组合函数会利用这个更改做一些逻辑。这样是不靠谱的,因为可组合函数的执行顺序是不确定的。

其他比较危险的附带效应包括:

  • 写入共享对象的属性,这里是指共享对象会存在同步问题,因为无法确保属性的写入和读取执行顺序,所以会产生异常结果。
  • 更新 ViewModel 中的可观察项,本质上也是共享的数据,会存在同步问题。
  • 更新 SharedPreference。

可组合函数可以并行运行

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

这种优化意味着,可组合函数可能会在后台线程池中执行。如果某个可组合函数对 ViewModel 调用一个函数,则 Compose 可能会同时从多个线程调用该函数。

为了确保应用正常运行,所有可组合函数都不应有附带效应,而应通过始终在界面线程上执行的 onClick 等回调触发附带效应。

调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免使用修改可组合 lambda 中的变量的代码,既因为此类代码并非线程安全代码,又因为它是可组合 lambda 不允许的附带效应。

重组会跳过尽可能多的内容

如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行界面树中在其上面或下面的任何可组合项。

例如:

kotlin 复制代码
/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.bodyLarge)
        Divider()
​
        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}
​
/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

也就是说,一个可组合函数中的更新会根据参数的变化进行重组,没有更新的参数会跳过执行。

本质上来说,这个函数中的代码还是会全部执行一遍,但因为 Text 和 LazyColumn 都是一个独立的可组合函数,他们也会检测自己的参数是否有更新,如果没有更新,会跳过内部逻辑执行。

重组是乐观的操作

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

取消重组后,Compose 会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组合操作,也会应用该附带效应。这可能会导致应用状态不一致(举例说明:就是重组执行一半被取消了,但更新共享对象的值已经更新完成了,所以也会产生附带效应)。

确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。

可组合函数可能会非常频繁地运行

在某些情况下,可能会针对界面动画的每一帧运行一个可组合函数。如果该函数执行成本高昂的操作(例如从设备存储空间读取数据),可能会导致界面卡顿。

例如,如果您的 widget 尝试读取设备设置,它可能会在一秒内读取这些设置数百次,这会对应用的性能造成灾难性的影响。

如果您的可组合函数需要数据,它应为相应的数据定义参数。然后,您可以将成本高昂的工作移至组成操作线程之外的其他线程,并使用 mutableStateOfLiveData 将相应的数据传递给 Compose。

总结

Compose 本质上是一套 Android UI 开发 API,UI 的抽象不再以对象的形式描述,而是变成了方法的形式。

这样的好处是,方法不会维护自己的内部状态,所有的状态应该由参数传递进来,也就是由逻辑层提供。所以分层更加清晰。

当数据变化时,Compose 会自动感知数据变化,然后去调用发生变化的可组合函数。因为重组整个界面树在计算上成本高昂,Compose 框架支持可以智能地仅重组已更改的组件。

在使用 Compose 时,需要注意:

  • 可组合函数可以按任何顺序执行,不要在可组合函数之前存在依赖关系。

  • 可组合函数可以并行执行,从而提高流畅度。

  • 可组合函数会产生附带效应,一般是同步问题,但不能通过同步框架来解决,因为这会影响重组的效率,最好的方式是把它们放在业务逻辑层处理,而不要在可组合函数中去处理。

  • 重组是乐观操作,当一次重组未执行完成时触发了下一次重组,会放弃之前的重组直接执行新的重组,但因为之前的重组执行了一半,已执行完成的部分(比如更新一个属性的值)不会回滚,依然生效。

  • 当数据频繁更新的情况下,可组合函数也可能会非常频繁的执行,这样可能会造成卡顿。此时最好使用 Compose 的状态相关的 API 来确保状态只在必要时更新,避免引发不必要的重组。

    • mutableStateOf 可以拆解状态,从而避免全局状态变化,细化重组粒度(只更新一个属性,对应的需要更新的 Widget 变少)。
    • LiveData 具有生命周期感知和避免重复内容更新的能力,有效减少重组的时机和次数。
相关推荐
model200524 分钟前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏68934 分钟前
Android广播
android·java·开发语言
与衫2 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了8 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵9 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru14 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng15 小时前
android 原生加载pdf
android·pdf
hhzz15 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
火红的小辣椒16 小时前
XSS基础
android·web安全
勿问东西18 小时前
【Android】设备操作
android