来自 Twitter 的 17 条 Compose 开发规范和检查工具:帮你避坑~

在这里插入图片描述

翻译自:

https://twitter.github.io/compose-rules/rules/

前言

对于大型团队来说,刚开始采用 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 entitiesNaming @Composable functions that return values

该 rule 的源码:twitter-compose:naming-check

11. 有序定义可组合函数的参数

在 Kotlin 中编写函数的时候,一个好的做法是先写必选参数,然后再写可选参数(即有默认值的参数)。这样做的话,我们可以最大限度地减少需要明确写出参数的次数,提高编码效率。

Modifier 通常会占据可选参数的第 1 个槽位,便可以为开发者提供统一的编码规范:即开发者可以始终提供一个 Modifier 实例作为元素调用的位置参数。

更多信息可以参考: Kotlin default arguments, Modifier docsElements 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 CastilloComposed modifiers in API guidelines

结语

如上的 rules 是 Twitter 使用 Compose 开发多年以来,不断结合官方文档和实战总结出来的宝贵经验。

如果想要使用该规则去检测代码是否合适,可以使用 ktlintDetekt 来导入规则和部署检查:

下一篇文章,我将采用上述 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 技术的发展来说,都是意义非凡的事情。

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值