查看原文
其他

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

小虾米君 郭霖
2024-07-19



/   今日科技快讯   /


近日,特斯拉股东预计将投票批准向该公司CEO埃隆·马斯克支付560亿美元的薪酬方案,同时允许将这家电动汽车制造商的法定总部迁至美国得克萨斯州。尽管正式的投票结果尚未公布,但马斯克已经提前宣布“获胜”。他周三在社交媒体平台X上表示,该决议以较大优势通过。他感谢股东的支持。


/   作者简介   /


明天周六,提前祝大家周末愉快!


本篇文章转载自TechMerger的博客,文章主要翻译了Twitter的Compose开发规范和检查工具,相信会对大家有所帮助!


原文地址:

https://juejin.cn/post/7377546788021207080


/   前言   /


翻译自: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 或 MutableState 实例


取而代之的是,可以向 Composable 函数传递相关的数据以及用于回调的 lambda。

更多信息可以查看 Compose的Compose和状态文档(https://developer.android.com/jetpack/compose/state)。


该 rule 的源码:twitter-compose:vm-forwarding-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeViewModelForwarding.kt)


2. 记住状态


通过 mutableStateOf 或任何其他的 State builder 构建 State 实例的时候,需要注意:确保代码中 remember 了这个 State 实例。否则,在 Composable 函数重组时,就会构建出一个新的 State 实例。


该 rule 的源码:twitter-compose:remember-missing-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeRememberMissing.kt)


3. 使用 @Immutable


Compose 编译器会去推断相关数据的不可变性 immutable 和稳定性 stable,但有时候这种判断会出错,这就会造成 UI 界面会多做些不必要的刷新工作。所以,如果想让编译器将某个类视为 "不可变"的,最好直接给该类使用 @Immutable 注解。

更多信息可以参考:不可变文档(https://developer.android.com/reference/kotlin/androidx/compose/runtime/Immutable)和可组合度量(https://chris.banes.dev/composable-metrics/)博文两篇文档


相关规则:尚无


4. 不使用不稳定的集合声明


Kotlin 中,集合 Collections 被定义为接口类型,例如:List, Map, Set。而他们的内部数据是否可变,是无法保证的。


举个栗子:


val list:List<String> = mutableListOf<String>()


变量 list 在声明的时候采用的类型是 val,意味着不可重新赋值,但其实 list 内部成员是可以改变的。


Compose 编译器在处理这种类型的变量时,虽然看到了 val 声明,但因无法准确判断其内容是否会发生变化,便会将该变量判定为不稳定。


要想强制让编译器将该集合判定为真正的"不可变",有这么几个方案,可以参考:Kotlinx 不可变集合(https://github.com/Kotlin/kotlinx.collections.immutable)文档。


比如采用 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 稳定性详解(https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8), Kotlinx 不可变集合(https://github.com/Kotlin/kotlinx.collections.immutable)两篇文档。


该 rule 的源码:twitter-compose:unstable-collections(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeUnstableCollections.kt)


/   Composables可组合函数   /


5. 不采用可变类型作函数参数


本条规则是由上面提到的“状态提升”规则延伸出来的。


“状态提升”规则里我们提到状态是向下流动的,可事实上很多开发者会情不自禁地将可变的 State 传递到函数里直接去改变它的值。但这是一种违反模式的做法,因为它破坏了状态向下流动、事件向上触发的模式。


值的改变作为一种事件,它应当在函数 API(lambda 回调)中进行构建。这样做的一个重要理由是:Compose 里极容易发生更新了可变对象却没有触发重组的情况。因为如果没能触发重组,可组合函数就不会被自动更新,进而无法反映更新后的值到 UI 上去。


常常被传递给可组合函数作为可变参数的,包括但不仅限于:ArrayList、MutableState 和 ViewModel。


该 rule 的源码: twitter-compose:mutable-params-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeMutableParameters.kt)


6. 不要同时发射布局又返回结果


可组合函数应该只发射布局内容,或者只返回某个结果。但不能两个都做,这样会显得混乱。


另外,如果可组合函数需要为调用方提供额外的界面控制,则这些控制逻辑或回调应作为参数由调用方提供给可组合函数。


更多信息可以参考: Compose API guidelines(https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#emit-xor-return-a-value)


该 rule 的源码:twitter-compose:content-emitter-returning-values-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeMultipleContentEmitters.kt)


注意:你可以将 composeEmitters 添加到 Detekt 规则配置中,或将 compose_emitters 添加到 ktlint 中的 .editorconfig 配置中。


7. 不要发射多片段的布局节点


一个可组合函数可以不发射或者只发射 1 段布局片段,切忌过多。因为可组合函数应当具备内聚性,而不应依赖于调用的函数。


下面是一个错误的示范:InnerContent() 函数会发出多个布局节点,并设想它该被 Column 的布局所调用。


 Column {
     InnerContent()
 }
 
 @Composable
 private fun InnerContent() {
     Text(...)
     Image(...)
     Button(...)
 }


与传统的 View 视图系统相比,Compose 布局嵌套的成本要低得多,因此开发者不需要去刻意地简化界面层级,甚至牺牲了正确性。


这条规则有一个小小的例外,那就是当可组合函数被定义为一个特定作用域扩展函数的时候,比如如下:


 @Composable
 private fun ColumnScope.InnerContent() {
     Text(...)
     Image(...)
     Button(...)
 }


这段代码将多个片段的布局有效地绑定到了从 Column 中调用的函数,尽管允许这样编码,但其实不推荐。


该 rule 的源码:twitter-compose:multiple-emitters-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeMultipleContentEmitters.kt)


8. 恰当命名 CompositionLocals 变量


给 CompositionLocal 命名时,应使用形容词 "Local"作为前缀,后面跟一个描述性的名词,描述其持有的值。


这样就能非常清晰地知道某个值来自某个 CompositionLocal 。鉴于这些都是隐含的依赖关系,我们尽量在命名层面将它们清晰地表露出来。


更多信息可以参考:Naming CompositionLocals(https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#naming-compositionlocals)


该 rule 的源码:twitter-compose:compositionlocal-naming(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeCompositionLocalNaming.kt)


9. 恰当命名 multipreview 注解


当自定义用于多个预览的注解时,其命名应使用 Previews 作为后缀。给这些注解明确的命名,可以确保在使用的时候,开发者能清楚地知道它们是 @Preview 的多个组合。


更多信息可以参考: Multipreview annotations(https://developer.android.com/jetpack/compose/tooling#preview-multipreview)


该 rule 的源码:twitter-compose:preview-naming(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposePreviewNaming.kt)


10. 恰当命名可组合函数


当可组合函数是 Unit 类型的时候,其命名应当以大写字母开头。它们被视为声明性实体,在组合中可以存在、也可以不存在,因此需要遵循类 class 的命名规则。


但是,带返回值的可组合函数应该以小写字母开头,应遵循 Kotlin Coding Conventions (https://kotlinlang.org/docs/reference/coding-conventions.html#function-names)中关于函数命名的规则。


更多信息可以参考: Naming Unit @Composable functions as entities(https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#naming-unit-composable-functions-as-entities) 和 Naming @Composable functions that return values(https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#naming-composable-functions-that-return-values)


该 rule 的源码:twitter-compose:naming-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeNaming.kt)


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


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


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


更多信息可以参考: Kotlin default arguments(https://kotlinlang.org/docs/functions.html#default-arguments), Modifier docs(https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier)和 Elements accept and respect a Modifier parameter(https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#why-8).


该 rule 的源码:twitter-compose:param-order-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeParameterOrder.kt)


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(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeViewModelInjection.kt)


  • CompositionLocals


CompositionLocal 使可组合函数的行为更难推理。由于它们会创建隐式依赖关系,调用它们的可组合函数需要确保每个 CompositionLocal 的值都得到满足。


虽然它们并不常见,但也有合法用例(https://developer.android.com/jetpack/compose/compositionlocal#deciding),因此本规则提供了一个允许列表,开发者可以将自己的 "CompositionLocal" 名称添加到该列表中,这样规则脚本就会将他们除外。


该 rule 的源码:twitter-compose:compositionlocal-allowlist(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/CompositionLocalAllowlist.kt)


注意:要将自定义的 CompositionLocal 添加到允许列表中,可以在 Detekt 的规则配置中添加 allowedCompositionLocals 或在 ktlint 的 .editorconfig 中添加 allowed_composition_locals 。


13. 声明仅支持预览的函数为 private


当一个可组合函数仅仅拥有 @Preview 注解,不会在实际的用户界面中调用的话,它不需要被声明为 public 的。同时,为防止其他开发者在不知情的情况下使用了它,我们应该将其可见性限制为private。


该 rule 的源码:twitter-compose:preview-public-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposePreviewPublic.kt)


注意: 如果您使用 Detekt,这可能会与 Detekt 的 UnusedPrivateMember 规则(https://detekt.dev/docs/rules/style/#unusedprivatemember)冲突。请务必将 Detekt 的 ignoreAnnotated 配置(https://detekt.dev/docs/introduction/compose/#unusedprivatemember)设置为['预览'],以便与此规则兼容。


/   Modifiers修饰符   /


14. 尽量提供 Modifier 参数


为了实现开发者将逻辑和行为自由附加到 Compose UI 上的目的,Compose 推出了组合而非继承的理念。Modifier 则是实现这个理念的最重要组件。


Modifier 对所有公共的 UI 组件都很重要,通过它,调用者便可以按照自己的意愿定制组件的各种组合。


更多信息可以参考: Always provide a Modifier parameter(https://chris.banes.dev/posts/always-provide-a-modifier/)


该 rule 的源码:twitter-compose:modifier-missing-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeModifierMissing.kt)


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(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeModifierReused.kt)


16. Modifier 应当具备默认的参数


可将 Modifier 作为参数应用于所代表的整个组件的可组合函数,应命名该参数为 modifier,并分配 Modifier 参数的默认值。它应当声明为参数列表中的第 1 个可选参数,且位于所有必选参数(尾部的 lambda 参数除外)之后,但应位于任何其他具有默认值的参数之前。


在可组合函数的实现中,可组合函数所需的任何默认 Modifier 都应位于 Modifier 参数值之后,并将 Modifier 保留为默认参数值。


更多信息可以参考: Modifier documentation(https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier)


该 rule 的源码:twitter-compose:modifier-without-default-check(https://github.com/twitter/compose-rules/blob/main/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeModifierWithoutDefault.kt)


17. 避免使用扩展函数构建 Modifier


不推荐在可组合函数里使用常用的扩展函数去构造 Modifier 实例,因为它们会导致不必要的重组。为避免该情况,推荐使用 Modifier.composed,因为它会将重组限制在 Modifier 实例上,而不是针对整个函数 tree。


而且 Composed Modifier 可能在组合之外创建出来、跨组件之间共享、并声明为顶层常量,这使得它们比在可组合函数里调用扩展函数创建的 Modifier 更灵活,也更容易避免意外地跨组件共享状态数据。


更多信息可以参考: Modifier extensions(https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#extension-functions), Composed modifiers in Jetpack Compose by Jorge Castillo(https://jorgecastillo.dev/composed-modifiers-in-jetpack-compose) 和 Composed modifiers in API guidelines(https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#composed-modifiers)


/   结语   /


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


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


  • ktlint:参考 Using with ktlint 文档(https://twitter.github.io/compose-rules/ktlint)

  • Detekt:参考 Using with Detekt 文档(https://twitter.github.io/compose-rules/detekt)


规则文档的开源地址


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 技术的发展来说,都是意义非凡的事情。



推荐阅读:
我的新书,《第一行代码 第3版》已出版!
两台Android 设备同一个局域网下如何自由通信?
Koltin中的变与不变

欢迎关注我的公众号
学习技术或投稿

长按上图,识别图中二维码即可关注
继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存