乘风破浪,遇见Android Jetpack之Compose声明式UI开发工具包,逐渐大一统的原生UI绘制体系

2023-02-16,,,,

什么是Android Jetpack

https://developer.android.com/jetpack

Android Jetpack是一个由多个库组成的套件,可帮助开发者遵循最佳做法、减少样板代码并编写可在各种Android版本和设备中一致运行的代码,让开发者可将精力集中于真正重要的编码工作。

什么是Jetpack Compose

Jetpack Compose是用于构建原生Android界面的新工具包。它可简化并加快Android上的界面开发,帮助您使用更少的代码、强大的工具和直观的Kotlin API,快速打造生动而精彩的应用。

https://developer.android.com/jetpack/compose

Jetpack Compose是用于构建原生界面的最新的Android工具包,采用声明式UI的设计,拥有更简单的自定义和实时的交互预览功能,由Android官方团队全新打造的UI框架。

为什么采用Compose

Jetpack Compose是用于构建原生Android界面的新工具包。它使用更少的代码、强大的工具和直观的Kotlin API,可以帮助您简化并加快Android界面开发,打造生动而精彩的应用。它可让您更快速、更轻松地构建Android界面。

更少的代码

编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现bug的可能性也更小,您就可以专注于解决手头的问题;作为审核人员或维护人员,您需要阅读、理解、审核和维护的代码就更少。

与使用AndroidView系统(按钮、列表或动画)相比,Compose可让您使用更少的代码实现更多的功能。无论您需要构建什么内容,现在需要编写的代码都更少了。以下是我们的一些合作伙伴的感想:

“对于相同的Button类,代码的体量要小10倍。”(Twitter)
“使用RecyclerView构建的任何屏幕(我们的大部分屏幕都使用它构建)的大小也显著减小。”(Monzo)
““只需要很少几行代码就可以在应用中创建列表或动画,这一点令我们非常满意。对于每项功能,我们编写的代码行更少了,这让我们能够将更多精力放在为客户提供价值上。”(Cuvva)
编写代码只需要采用Kotlin,而不必拆分成Kotlin和XML部分:“当所有代码都使用同一种语言编写并且通常位于同一文件中(而不是在Kotlin和XML语言之间来回切换)时,跟踪变得更容易”(Monzo)

无论您要构建什么,使用Compose编写的代码都很简洁且易于维护。“Compose的布局系统在概念上更简单,因此可以更轻松地推断。查看复杂组件的代码也更轻松。”(Square)

直观

Compose使用声明性API,这意味着您只需描述界面,Compose会负责完成其余工作。这类API十分直观-易于探索和使用:“我们的主题层的直观和清晰程度显著提高。我们能够在单个Kotlin文件中完成之前需要在多个XML文件中完成的任务,这些XML文件负责通过多个分层主题叠加层定义和分配属性。”(Twitter)

利用Compose,您可以构建不与特定activity或fragment相关联的小型无状态组件。这让您可以轻松重用和测试这些组件:“我们给自己设定的目标是,交付一组新的无状态界面组件,确保它们易于使用和维护,且可直观实现/扩展/自定义。就这一点而言,Compose确实为我们提供了一个可靠的答案。”(Twitter)

在Compose中,状态是显式的,并且会传递给相应的可组合项。这样一来,状态便具有单一可信来源,因而是封装和分离的。然后,应用状态变化时,界面会自动更新。“在对某些内容进行推断时,不必处理太多信息,并且无法控制或难以理解的行为也更少”(Cuvva)

加快应用开发

Compose与您所有的现有代码兼容:您可以从View调用Compose代码,也可以从Compose调用View。大多数常用库(如Navigation、ViewModel和Kotlin协程)都适用于Compose,因此您可以随时随地开始采用。“我们一开始集成Compose是为了实现互操作性,并且这样确实‘行之有效’。我们发现,我们不必考虑浅色模式和深色模式等问题,整个体验无比顺畅。”(Cuvva)

借助全面的AndroidStudio支持以及实时预览等功能,您可以更快地迭代和交付代码:“AndroidStudio中的预览功能极大地节省了我们的时间。能够构建多个预览也帮我们节省了时间。我们通常需要检查不同状态下或采用不同设置的界面组件(例如错误状态或采用不同的字体大小等)。由于能够创建多个预览,我们可以轻松执行这些检查。”(Square)

功能强大

利用Compose,您可以凭借对Android平台API的直接访问和对于MaterialDesign、深色主题、动画等的内置支持,创建精美的应用:“Compose不仅解决了声明性界面的问题,还改进了无障碍功能API、布局等各种内容。将设想变为现实所需的步骤更少了”(Square)。

利用Compose,您可以轻松快速地通过动画让应用变得生动有趣:“在Compose中添加动画非常简单,没有理由不去为颜色/大小/高度变化添加动画效果”(Monzo),“不需要任何特殊的工具就能制作动画,这与显示静态屏幕没有什么不同”(Square)。

无论您是使用MaterialDesign还是自己的设计系统进行构建,Compose都可以让您灵活地实现所需的设计:“从基础上将MaterialDesign分离出来对我们来说非常有用,因为我们要构建自己的设计系统,这往往需要与Material不同的设计要求。”(Square)

使用入门

Jetpack Compose教程

https://developer.android.com/jetpack/compose/tutorial

将Android Studio与Jetpack Compose配合使用

为了在使用Jetpack Compose进行开发时获得最佳体验,您应下载最新版本的Android Studio Arctic Fox。这是因为,当您搭配使用Android Studio和Jetpack Compose开发应用时,可以从智能编辑器功能中受益,这些功能包括“新建项目”模板和即时预览Compose界面等。

下载Android Studio Arctic Fox

安装Android Studio后,请按照以下说明尝试Jetpack Compose示例应用,创建新的Jetpack Compose应用项目,或者向现有应用项目添加对Jetpack Compose的支持。

Jetpack Compose示例

https://github.com/android/compose-samples

如若要运行示例,至少需要使用Android Studio Arctic Fox

示例导入Android Studio指南:https://developer.android.com/jetpack/compose/setup#sample

创建支持Jetpack Compose的新应用

https://developer.android.com/jetpack/compose/setup#create-new

如果您想要创建一个默认支持 Jetpack Compose 的新项目,Android Studio 提供了新项目模板来帮助您入手。如需创建支持 Jetpack Compose 的新项目,请按以下步骤操作:

    如果您位于 Welcome to Android Studio 窗口中,请点击 Start a new Android Studio project。如果您已打开 Android Studio 项目,请从菜单栏中依次选择 File > New > New Project。
    在 Select a Project Template 窗口中,选择 Empty Compose Activity,然后点击 Next。
    在 Configure your project 窗口中,执行以下操作:
    按照常规方法设置 Name、Package name 和 Save location。
    请注意,在 Language 下拉菜单中,Kotlin 是唯一可用的选项,因为 Jetpack Compose 仅适用于使用 Kotlin 编写的类。
    在 Minimum API level dropdown 菜单中,选择 API 级别 21 或更高级别。
    点击 Finish。
    根据配置 Gradle 中所述的方法,验证项目的 build.gradle 文件配置是否正确。

将Jetpack Compose添加到现有项目中

https://developer.android.com/jetpack/compose/setup#add-compose

编程思想

Jetpack Compose是一个适用于Android的新式声明性界面工具包。Compose提供声明性API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。此术语需要一些解释说明,它的含义对应用设计非常重要。

声明性编程范式

长期以来,Android视图层次结构一直可以表示为界面微件树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用findViewById()等函数遍历树,并通过调用button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap)等方法更改节点。这些方法会改变微件的内部状态。

手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以意外的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护复杂性会随着需要更新的视图数量而增长。

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

重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减轻这一成本,Compose会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。

简单的可组合函数

使用Compose,您可以通过定义一组接受数据而发出界面元素的可组合函数来构建界面。一个简单的示例是Greeting微件,它接受String而发出一个显示问候消息的Text微件。

显示文本“HelloWorld”的手机的屏幕截图,以及用于生成该界面的简单可组合函数的代码

图1.一个简单的可组合函数,系统向它传递了数据,它使用该数据在屏幕上呈现文本微件。

关于此函数,有几点值得注意:

此函数带有@Composable注释。所有可组合函数都必须带有此注释;此注释可告知Compose编译器:此函数旨在将数据转换为界面。

此函数接受数据。可组合函数可以接受一些参数,这些参数可让应用逻辑描述界面。在本例中,我们的微件接受一个String,因此它可以按名称问候用户。

此函数可以在界面中显示文本。为此,它会调用Text()可组合函数,该函数实际上会创建文本界面元素。可组合函数通过调用其他可组合函数来发出界面层次结构。

此函数不会返回任何内容。发出界面的Compose函数不需要返回任何内容,因为它们描述所需的屏幕状态,而不是构造界面微件。

此函数快速、幂等且没有副作用。

使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对random()的调用。
此函数描述界面而没有任何副作用,如修改属性或全局变量。
一般来说,出于重组部分所述的原因,所有可组合函数都应使用这些属性来编写。

声明性范式转变

在许多面向对象的命令式界面工具包中,您可以通过实例化微件树来初始化界面。您通常通过膨胀XML布局文件来实现此目的。每个微件都维护自己的内部状态,并且提供getter和setter方法,允许应用逻辑与微件进行交互。

在Compose的声明性方法中,微件相对无状态,并且不提供setter或getter函数。实际上,微件不会以对象形式提供。您可以通过调用带有不同参数的同一可组合函数来更新界面。这使得向架构模式(如ViewModel)提供状态变得很容易,如应用架构指南中所述。然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。

图2.应用逻辑为顶级可组合函数提供数据。该函数通过调用其他可组合函数来使用这些数据描述界面,将适当的数据传递给这些可组合函数,并沿层次结构向下传递数据。

当用户与界面交互时,界面会发起onClick等事件。这些事件应通知应用逻辑,应用逻辑随后可以改变应用的状态。当状态发生变化时,系统会使用新数据再次调用可组合函数。这会导致重新绘制界面元素,此过程称为“重组”。

说明界面元素如何通过触发由应用逻辑处理的事件来响应交互的图示。

图3.用户与界面元素进行了交互,导致触发一个事件。应用逻辑响应该事件,然后系统根据需要使用新参数自动再次调用可组合函数。

动态内容

由于可组合函数是用Kotlin而不是XML编写的,因此它们可以像其他任何Kotlin代码一样动态。例如,假设您想要构建一个界面,用来问候一些用户:

@Composable
fun Greeting(names: List<String>) {
for (name in names) {
Text("Hello $name")
}
}

此函数接受名称的列表,并为每个用户生成一句问候语。可组合函数可能非常复杂。您可以使用if语句来确定是否要显示特定的界面元素。您可以使用循环。您可以调用辅助函数。您拥有底层语言的全部灵活性。这种强大的功能和灵活性是Jetpack Compose的主要优势之一。

重组

在命令式界面模型中,如需更改某个微件,您可以在该微件上调用setter以更改其内部状态。在Compose中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组--系统会根据需要使用新数据重新绘制函数发出的微件。Compose框架可以智能地仅重组已更改的组件。

例如,假设有以下可组合函数,它用于显示一个按钮:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}

每次点击该按钮时,调用方都会更新clicks的值。Compose会再次调用lambda与Text函数以显示新值;此过程称为“重组”。不依赖于该值的其他函数不会进行重组。

如前文所述,重组整个界面树在计算上成本高昂,因为会消耗计算能力并缩短电池续航时间。Compose使用智能重组来解决此问题。

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

切勿依赖于执行可组合函数所产生的附带效应,因为可能会跳过函数的重组。如果您这样做,用户可能会在您的应用中遇到奇怪且不可预测的行为。附带效应是指对应用的其余部分可见的任何更改。例如,以下操作全部都是危险的附带效应:

写入共享对象的属性
更新ViewModel中的可观察项
更新共享偏好设置

可组合函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。可组合函数应快速执行,以避免在播放动画期间出现卡顿。如果您需要执行成本高昂的操作(例如从共享偏好设置读取数据),请在后台协程中执行,并将值结果作为参数传递给可组合函数。

例如,以下代码会创建一个可组合项以更新SharedPreferences中的值。该可组合项不应从共享偏好设置本身读取或写入,于是此代码将读取和写入操作移至后台协程中的ViewModel。应用逻辑会使用回调传递当前值以触发更新。

@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}

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

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

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

@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}

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

可组合函数可以并行运行

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

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

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

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

以下示例展示了一个可组合项,它显示一个列表及其项数:

@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}

此代码没有附带效应,它会将输入列表转换为界面。此代码非常适合显示小列表。不过,如果函数写入局部变量,则这并非线程安全或正确的代码:

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
Text("Count: $items")
}
}

在本例中,每次重组时,都会修改items。这可以是动画的每一帧,或是在列表更新时。但不管怎样,界面都会显示错误的项数。因此,Compose不支持这样的写入操作;通过禁止此类写入操作,我们允许框架更改线程以执行可组合lambda。

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

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

每个可组合函数和lambda都可以自行重组。以下示例演示了在呈现列表时重组如何跳过某些元素:

/**
* 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.h5)
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) }))
}

这些作用域中的每一个都可能是在重组期间执行的唯一一个作用域。当header发生更改时,Compose可能会跳至Columnlambda,而不执行它的任何父项。此外,执行Column时,如果names未更改,Compose可能会选择跳过LazyColumnItems。

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

重组是乐观的操作

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

取消重组后,Compose会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组成操作,也会应用该附带效应。这可能会导致应用状态不一致。

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

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

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

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

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

参考

https://developer.android.com/jetpack
Jetpack Compose 1.0 终于要投入使用了!
https://docs.compose.net.cn

乘风破浪遇见Android Jetpack之Compose声明式UI开发工具包,逐渐大一统的原生UI绘制体系的相关教程结束。

《乘风破浪,遇见Android Jetpack之Compose声明式UI开发工具包,逐渐大一统的原生UI绘制体系.doc》

下载本文的Word格式文档,以方便收藏与打印。