来自 Twitter 的 17 条 Compose 开发规范和检查工具:帮你避坑~
翻译自:
前言
对于大型团队来说,刚开始采用 Compose
开发的时候,会面临很多的挑战。尤其每个开发者对 Compose 的认知不同:接触的时间或长或短、开发的水平也参差不齐。
Twitter 计划通过创建一套 Compose rules 来解决这些痛点。经过一段时间的探索之后,Twitter 推出了一套自定义的 Compose 静态检查 rules,可以确保开发者编写的 Composables 函数避免一些常见的错误。
的确,Compose 技术有很多超能力,但也存在很多容易犯的错(坑),这时候上面的静态检测 rules 便可以派上用场了。我们期望这些 rules 可以在正式 review 代码之前,便帮助开发者检测出尽可能多的、潜在的 Compose 使用问题,从而促进 Compose 技术的健康发展!
State 状态相关
1. 保持状态的提升
有一种设计理念叫做“单向数据流”,它的特征是:状态下降、事件上升。Compose 技术也是建立在这种单向数据流理念上的,可以概括为:状态向下流动,事件向上触发。
为了实现这一点,Compose 主张尽量保持状态的提升,从而使得大部分的可组合函数都是不具备状态,这样做有很多好处,比如更加解耦、易于测试。
在实践中,还有一些注意点需要留意:
- 不要向下传递
ViewModels
或来自 DI 带来的实例 - 不要向下传递
State<Foo>
或MutableState<Bar>
实例
取而代之的是,可以向 Composable 函数传递相关的数据以及用于回调的 lambda。
更多信息可以查看 Compose 的Compose 和状态文档。
该 rule 的源码:twitter-compose:vm-forwarding-check
2. 记住状态
通过 mutableStateOf
或任何其他的 State builder 构建 State 实例的时候,需要注意:确保代码中 remember
了这个 State 实例。否则,在 Composable 函数重组时,就会构建出一个新的 State 实例。
该 rule 的源码:twitter-compose:remember-missing-check
3. 使用 @Immutable
Compose 编译器会去推断相关数据的不可变性 immutable 和稳定性 stable,但有时候这种判断会出错,这就会造成 UI 界面会多做些不必要的刷新工作。所以,如果想让编译器将某个类视为 "不可变"的,最好直接给该类使用 @Immutable
注解。
相关规则: 尚无
4. 不使用不稳定的集合声明
Kotlin 中,集合 Collections
被定义为接口类型,例如:List<T>
, Map<T>
, Set<T>
。而他们的内部数据是否可变,是无法保证的。
举个栗子:
val list: List<String> = mutableListOf<String>()
变量 list
在声明的时候采用的类型是 val,意味着不可重新赋值,但其实 list 内部成员是可以改变的。
Compose 编译器在处理这种类型的变量时,虽然看到了 val 声明,但因无法准确判断其内容是否会发生变化,便会将该变量判定为不稳定。
要想强制让编译器将该集合判定为真正的"不可变",有这么几个方案,可以参考:Kotlinx 不可变集合文档。
比如采用 ImmutableList
接口的类型进行声明。
val list: ImmutableList<String> = persistentListOf<String>()
或者,将集合封装在一个带注解 @Immutable
的稳定类中。
@Immutable
data class StringList(val items: List<String>)
// ...
val list: StringList = StringList(yourList)
注意: 最好使用 Kotlinx 中定义的不可变集合接口类型和方法。因为你可能也发现了,虽然后者通过注解强调了它是不可变的,但其实其内部的 List 仍然是可变的。
更多信息可以参考:Jetpack Compose 稳定性详解, Kotlinx 不可变集合 两篇文档。
该 rule 的源码:twitter-compose:unstable-collections
Composables 可组合函数相关
5. 不采用可变类型作函数参数
本条规则是由上面提到的“状态提升”规则延伸出来的。
“状态提升”规则里我们提到状态是向下流动的,可事实上很多开发者会情不自禁地将可变的 State 传递到函数里直接去改变它的值。但这是一种违反模式的做法,因为它破坏了状态向下流动、事件向上触发的模式。
值的改变作为一种事件,它应当在函数 API(lambda 回调)中进行构建。这样做的一个重要理由是:Compose 里极容易发生更新了可变对象却没有触发重组的情况。因为如果没能触发重组,可组合函数就不会被自动更新,进而无法反映更新后的值到 UI 上去。
常常被传递给可组合函数作为可变参数的,包括但不仅限于:ArrayList<T>
、MutableState<T>
和 ViewModel
。
该 rule 的源码: twitter-compose:mutable-params-check
6. 不要同时发射布局又返回结果
可组合函数应该只发射布局内容,或者只返回某个结果。但不能两个都做,这样会显得混乱。
另外,如果可组合函数需要为调用方提供额外的界面控制,则这些控制逻辑或回调应作为参数由调用方提供给可组合函数。
更多信息可以参考: Compose API guidelines
该 rule 的源码: twitter-compose:content-emitter-returning-values-check
注意:你可以将
composeEmitters
添加到 Detekt 规则配置中,或将compose_emitters
添加到 ktlint 中的 .editorconfig 配置中。
7. 不要发射多片段的布局节点
一个可组合函数可以不发射或者只发射 1 段布局片段,切忌过多。因为可组合函数应当具备内聚性,而不应依赖于调用的函数。
下面是一个错误的示范:InnerContent() 函数会发出多个布局节点,并设想它该被 Column
的布局所调用。
Column {
InnerContent()
}
@Composable
private fun InnerContent() {
Text(...)
Image(...)
Button(...)
}
然而,InnerContent 也可以很容易地从 Row 中调用,这将打破所有假设。相反,InnerContent 应具有内聚性,并且本身应发出一个布局节点:
@Composable
private fun InnerContent() {
Column {
Text(...)
Image(...)
Button(...)
}
}
与传统的 View 视图系统相比,Compose 布局嵌套的成本要低得多,因此开发者不需要去刻意地简化界面层级,甚至牺牲了正确性。
这条规则有一个小小的例外,那就是当可组合函数被定义为一个特定作用域扩展函数的时候,比如如下:
@Composable
private fun ColumnScope.InnerContent() {
Text(...)
Image(...)
Button(...)
}
这段代码将多个片段的布局有效地绑定到了从 Column
中调用的函数,尽管允许这样编码,但其实不推荐。
该 rule 的源码:twitter-compose:multiple-emitters-check
8. 恰当命名 CompositionLocals 变量
给 CompositionLocal
命名时,应使用形容词 "Local"作为前缀,后面跟一个描述性的名词,描述其持有的值。
这样就能非常清晰地知道某个值来自某个 CompositionLocal
。鉴于这些都是隐含的依赖关系,我们尽量在命名层面将它们清晰地表露出来。
更多信息可以参考:Naming CompositionLocals
该 rule 的源码:twitter-compose:compositionlocal-naming
9. 恰当命名 multipreview 注解
当自定义用于多个预览的注解时,其命名应使用 Previews
作为后缀。给这些注解明确的命名,可以确保在使用的时候,开发者能清楚地知道它们是 @Preview
的多个组合。
更多信息可以参考: Multipreview annotations
该 rule 的源码:twitter-compose:preview-naming
10. 恰当命名可组合函数
当可组合函数是 Unit 类型的时候,其命名应当以大写字母开头。它们被视为声明性实体,在组合中可以存在、也可以不存在,因此需要遵循类 class 的命名规则。
但是,带返回值的可组合函数应该以小写字母开头,应遵循 Kotlin Coding Conventions 中关于函数命名的规则。
更多信息可以参考: Naming Unit @Composable functions as entities 和 Naming @Composable functions that return values
该 rule 的源码:twitter-compose:naming-check
11. 有序定义可组合函数的参数
在 Kotlin 中编写函数的时候,一个好的做法是先写必选参数,然后再写可选参数(即有默认值的参数)。这样做的话,我们可以最大限度地减少需要明确写出参数的次数,提高编码效率。
Modifier
通常会占据可选参数的第 1 个槽位,便可以为开发者提供统一的编码规范:即开发者可以始终提供一个 Modifier 实例作为元素调用的位置参数。
更多信息可以参考: Kotlin default arguments, Modifier docs 和 Elements accept and respect a Modifier parameter.
该 rule 的源码:twitter-compose:param-order-check
12. 显示声明依赖关系
ViewModels
在设计可组合函数的时候,我们应尽量明确它们之间的依赖关系。如果在可组合函数的主体中,从 DI 获取 ViewModel 或某个实例,就等于隐式地产生了依赖关系,可这样做的缺点是难以测试、也难以复用。
为了解决这个问题,你应该在可组合函数中将这些依赖关系作为默认值注入。让我们举例说明:
@Composable
private fun MyComposable() {
val viewModel = viewModel<MyViewModel>()
}
上述这种可组合函数里,依赖关系是隐式的。在测试时,你需要用某种方式伪造 viewModel 的内部结构,以便获取你想要的 ViewModel 实例。
但是,如果将其改为通过函数参数传递这些实例,就可以在测试中直接提供所需的实例,不再需要额外的工作。这样做还有一个好处,就是可以在函数定义里明确声明其对外存在依赖关系。
@Composable
private fun MyComposable(
viewModel: MyViewModel = viewModel(),
) { ... }
该 rule 的源码:twitter-compose:vm-injection-check
CompositionLocals
CompositionLocal
使可组合函数的行为更难推理。由于它们会创建隐式依赖关系,调用它们的可组合函数需要确保每个 CompositionLocal 的值都得到满足。
虽然它们并不常见,但也有合法用例,因此本规则提供了一个允许列表,开发者可以将自己的 “CompositionLocal” 名称添加到该列表中,这样规则脚本就会将他们除外。
该 rule 的源码:twitter-compose:compositionlocal-allowlist
注意: 要将自定义的
CompositionLocal
添加到允许列表中,可以在 Detekt 的规则配置中添加allowedCompositionLocals
或在 ktlint 的 .editorconfig 中添加allowed_composition_locals
。
13. 声明仅支持预览的函数为 private
当一个可组合函数仅仅拥有 @Preview
注解,不会在实际的用户界面中调用的话,它不需要被声明为 public 的。同时,为防止其他开发者在不知情的情况下使用了它,我们应该将其可见性限制为private
。
该 rule 的源码:twitter-compose:preview-public-check
注意: 如果您使用 Detekt,这可能会与 Detekt 的 UnusedPrivateMember 规则 冲突。请务必将 Detekt 的 ignoreAnnotated 配置 设置为[‘预览’],以便与此规则兼容。
Modifiers 修饰符相关
14. 尽量提供 Modifier 参数
为了实现开发者将逻辑和行为自由附加到 Compose UI 上的目的,Compose 推出了组合而非继承的理念。Modifier 则是实现这个理念的最重要组件。
Modifier 对所有公共的 UI 组件都很重要,通过它,调用者便可以按照自己的意愿定制组件的各种组合。
更多信息可以参考: Always provide a Modifier parameter
该 rule 的源码:twitter-compose:modifier-missing-check
15. 不重复使用 Modifiers
传入的 Modifier 实例应由可组合函数内单个布局节点使用。如果所提供的 Modifiers 被不同层级的多个可组合函数所使用,可能会发生预期外的行为。
在下面的示例中,可组合函数定义了一个公共的 Modifier 参数,内部将其传递给根节点的 Column 组件。但同时在调用每个子组件的时候也传递了了该参数,并在基础上添加了一些额外的 Modifier:
@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
Column(modifier) {
Text(modifier.clickable(), ...)
Image(modifier.size(), ...)
Button(modifier, ...)
}
}
其实不建议这样编码,参数里的 Modifier 实例仅应该被用到 Column 组件上。子组件应使用通过空的 Modifier 单例对象新建的 Modifier 实例。
@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
Column(modifier) {
Text(Modifier.clickable(), ...)
Image(Modifier.size(), ...)
Button(Modifier, ...)
}
}
该 rule 的源码:twitter-compose:modifier-reused-check
16. Modifier 应当具备默认的参数
可将 Modifier 作为参数应用于所代表的整个组件的可组合函数,应命名该参数为 modifier,并分配 Modifier 参数的默认值。它应当声明为参数列表中的第 1 个可选参数,且位于所有必选参数(尾部的 lambda 参数除外)之后,但应位于任何其他具有默认值的参数之前。
在可组合函数的实现中,可组合函数所需的任何默认 Modifier 都应位于 Modifier 参数值之后,并将 Modifier
保留为默认参数值。
更多信息可以参考: Modifier documentation
该 rule 的源码:twitter-compose:modifier-without-default-check
17. 避免使用扩展函数构建 Modifier
不推荐在可组合函数里使用常用的扩展函数去构造 Modifier 实例,因为它们会导致不必要的重组。为避免该情况,推荐使用 Modifier.composed
,因为它会将重组限制在 Modifier 实例上,而不是针对整个函数 tree。
而且 Composed Modifier 可能在组合之外创建出来、跨组件之间共享、并声明为顶层常量,这使得它们比在可组合函数里调用扩展函数创建的 Modifier 更灵活,也更容易避免意外地跨组件共享状态数据。
更多信息可以参考: Modifier extensions, Composed modifiers in Jetpack Compose by Jorge Castillo 和 Composed modifiers in API guidelines
结语
如上的 rules 是 Twitter 使用 Compose 开发多年以来,不断结合官方文档和实战总结出来的宝贵经验。
如果想要使用该规则去检测代码是否合适,可以使用 ktlint
、Detekt
来导入规则和部署检查:
- ktlint:参考 Using with ktlint 文档
- Detekt:参考 Using with Detekt 文档
下一篇文章,我将采用上述 rules 对我在几年前 Compose 还未正式发布时写的 Compose 复刻 Flappy Bird 项目进行检查实操,敬请期待!
规则文档的开源地址
https://github.com/twitter/compose-rules
译者备注
不像 Java、Kotlin 这种由来已久的语言,已经有很多成熟的 rules,并被广泛认可和部署到大大小小的项目当中。
而像 Compose 这种新兴的、落地不多的项目来说,很多规则、建议都还在摸索当中,像 Twitter 这种大厂能够将开发心得无私地总结和开源出来,是非常难能可贵的。
可惜我在该项目的 issues 列表里看到一则提问:
The future of this project?
Twitter 的员工回复说:因为该 repo 核心人员的离职,本 repo 的未来不太明朗。
如今它最近的一次提交截止在 2023 年 1 月!
我衷心希望开发者们能向这个 repo 持续地贡献力量,让它壮大下去。
如果哪一天这个规范的部分或全部内容被广泛接受、纳入到 Compose 官方 rules 当中,那对 Compose 技术、Android UI 技术的发展来说,都是意义非凡的事情。
原文地址:https://blog.csdn.net/allisonchen/article/details/136994429
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!