以下文章来源于TechMerger ,作者小虾米君
Android 资深从业者,GDG 分享嘉宾,掘金签约作者。给您带来新颖的、有价值的 Android 行业文章。
翻译自:
https://twitter.github.io/compose-rules/rules/
❞
对于大型团队来说,刚开始采用 Compose
开发的时候,会面临很多的挑战。尤其每个开发者对 Compose 的认知不同:接触的时间或长或短、开发的水平也参差不齐。
Twitter 计划通过创建一套 Compose rules 来解决这些痛点。经过一段时间的探索之后,Twitter 推出了一套自定义的 Compose 静态检查 rules,可以确保开发者编写的 Composables 函数避免一些常见的错误。
的确,Compose 技术有很多超能力,但也存在很多容易犯的错(坑),这时候上面的静态检测 rules 便可以派上用场了。我们期望这些 rules 可以在正式 review 代码之前,便帮助开发者检测出尽可能多的、潜在的 Compose 使用问题,从而促进 Compose 技术的健康发展!
有一种设计理念叫做“单向数据流”,它的特征是:状态下降、事件上升。Compose 技术也是建立在这种单向数据流理念上的,可以概括为:状态向下流动,事件向上触发。
为了实现这一点,Compose 主张尽量保持《状态的提升》,从而使得大部分的可组合函数都是不具备状态,这样做有很多好处,比如更加解耦、易于测试。
在实践中,还有一些注意点需要留意:
ViewModels
或来自 DI 带来的实例State<Foo>
或 MutableState<Bar>
实例取而代之的是,可以向 Composable 函数传递相关的数据以及用于回调的 lambda。
更多信息可以查看:
该 rule 的名称和源码:vm-forwarding-check -> ComposeViewModelForwarding.kt
通过 mutableStateOf
或任何其他的 State builder 构建 State 实例的时候,需要注意:确保代码中 remember
了这个 State 实例。否则,在 Composable 函数重组时,就会构建出一个新的 State 实例。
该 rule 的名称和源码:remember-missing-check -> ComposeRememberMissing.kt
Compose 编译器会去推断相关数据的不可变性 immutable 和稳定性 stable,但有时候这种判断会出错,这就会造成 UI 界面会多做些不必要的刷新工作。所以,如果想让编译器将某个类视为 "不可变"的,最好直接给该类使用 @Immutable
注解。
更多信息可以参考:
Kotlin 中,集合 Collections
被定义为接口类型,例如:List<T>
, Map<T>
, Set<T>
。而他们的内部数据是否可变,是无法保证的。
举个栗子:
val list:List<String> = mutableListOf<String>()
变量 list
在声明的时候采用的类型是 val,意味着不可重新赋值,但其实 list 内部成员是可以改变的。
Compose 编译器在处理这种类型的变量时,虽然看到了 val 声明,但因无法准确判断其内容是否会发生变化,便会将该变量判定为不稳定。
要想强制让编译器将该集合判定为真正的"不可变",有这么几个方案,比如:采用 ImmutableList
接口的类型进行声明。
val list:ImmutableList<String> = persistentListOf<String>()
或者,将集合封装在一个带注解 @Immutable
的稳定类中。
@Immutable
data class StringList(val items: List<String>)
// ...
val list:StringList = StringList(yourList)
注意:最好使用 Kotlinx 中定义的不可变集合接口类型和方法。因为你可能也发现了,虽然后者通过注解强调了它是不可变的,但其实其内部的 List 仍然是可变的。
❞
更多信息可以参考:
该 rule 的名称和源码:unstable-collections -> ComposeUnstableCollections.kt
本条规则是由上面提到的“状态提升”规则延伸出来的。
❞
“状态提升”规则里我们提到状态是向下流动的,可事实上很多开发者会情不自禁地将可变的 State 传递到函数里直接去改变它的值。但这是一种违反模式的做法,因为它破坏了状态向下流动、事件向上触发的模式。
值的改变作为一种事件,它应当在函数 API(lambda 回调)中进行构建。这样做的一个重要理由是:Compose 里极容易发生更新了可变对象却没有触发重组的情况。因为如果没能触发重组,可组合函数就不会被自动更新,进而无法反映更新后的值到 UI 上去。
常常被传递给可组合函数作为可变参数的,包括但不仅限于:ArrayList<T>
、MutableState<T>
和 ViewModel
。
该 rule 的名称和源码:mutable-params-check -> ComposeMutableParameters.kt
可组合函数应该只发射布局内容,或者只返回某个结果。但不能两个都做,这样会显得混乱。
另外,如果可组合函数需要为调用方提供额外的界面控制,则这些控制逻辑或回调应作为参数由调用方提供给可组合函数。
更多信息可以参考:
该 rule 的名称和源码: content-emitter-returning-values-check -> ComposeMultipleContentEmitters.kt
注意:你可以将
❞composeEmitters
添加到 Detekt 规则配置中,或将compose_emitters
添加到 ktlint 中的 .editorconfig 配置中。
一个可组合函数可以不发射或者只发射 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 的名称和源码:multiple-emitters-check -> ComposeMultipleContentEmitters.kt
给 CompositionLocal
命名时,应使用形容词 "Local"作为前缀,后面跟一个描述性的名词,描述其持有的值。
这样就能非常清晰地知道某个值来自某个 CompositionLocal
。鉴于这些都是隐含的依赖关系,我们尽量在命名层面将它们清晰地表露出来。
更多信息可以参考:
该 rule 的名称和源码:compositionlocal-naming -> ComposeCompositionLocalNaming.kt
当自定义用于多个预览的注解时,其命名应使用 Previews
作为后缀。给这些注解明确的命名,可以确保在使用的时候,开发者能清楚地知道它们是 @Preview
的多个组合。
更多信息可以参考:
该 rule 的名称和源码:preview-naming -> ComposePreviewNaming.kt
当可组合函数是 Unit 类型的时候,其命名应当以大写字母开头。它们被视为声明性实体,在组合中可以存在、也可以不存在,因此需要遵循类 class 的命名规则。
但是,带返回值的可组合函数应该以小写字母开头,应遵循《Kotlin Coding Conventions》中关于函数命名的规则。
更多信息可以参考:
该 rule 的名称和源码:naming-check -> ComposeNaming.kt
在 Kotlin 中编写函数的时候,一个好的做法是先写必选参数,然后再写可选参数(即有默认值的参数)。这样做的话,我们可以最大限度地减少需要明确写出参数的次数,提高编码效率。
Modifier
通常会占据可选参数的第 1 个槽位,便可以为开发者提供统一的编码规范:即开发者可以始终提供一个 Modifier 实例作为元素调用的位置参数。
更多信息可以参考:
该 rule 的名称和源码:param-order-check -> ComposeParameterOrder.kt
ViewModels
在设计可组合函数的时候,我们应尽量明确它们之间的依赖关系。如果在可组合函数的主体中,从 DI 获取 ViewModel 或某个实例,就等于隐式地产生了依赖关系,可这样做的缺点是难以测试、也难以复用。
为了解决这个问题,你应该在可组合函数中将这些依赖关系作为默认值注入。让我们举例说明:
@Composable
private fun MyComposable() {
val viewModel = viewModel<MyViewModel>()
}
上述这种可组合函数里,依赖关系是隐式的。在测试时,你需要用某种方式伪造 viewModel 的内部结构,以便获取你想要的 ViewModel 实例。
但是,如果将其改为通过函数参数传递这些实例,就可以在测试中直接提供所需的实例,不再需要额外的工作。这样做还有一个好处,就是可以在函数定义里明确声明其对外存在依赖关系。
@Composable
private fun MyComposable(
viewModel: MyViewModel = viewModel(),
) { ... }
该 rule 的名称和源码:vm-injection-check -> ComposeViewModelInjection.kt
CompositionLocals
CompositionLocal
使可组合函数的行为更难推理。由于它们会创建隐式依赖关系,调用它们的可组合函数需要确保每个 CompositionLocal 的值都得到满足。
虽然它们并不常见,但也有《合法用例》,因此本规则提供了一个允许列表,开发者可以将自己的 "CompositionLocal" 名称添加到该列表中,这样规则脚本就会将他们除外。
该 rule 的名称和源码:compositionlocal-allowlist -> CompositionLocalAllowlist.kt
注意:要将自定义的
❞CompositionLocal
添加到允许列表中,可以在 Detekt 的规则配置中添加allowedCompositionLocals
或在 ktlint 的 .editorconfig 中添加allowed_composition_locals
。
当一个可组合函数仅仅拥有 @Preview
注解,不会在实际的用户界面中调用的话,它不需要被声明为 public 的。同时,为防止其他开发者在不知情的情况下使用了它,我们应该将其可见性限制为private
。
该 rule 的名称和源码:preview-public-check -> ComposePreviewPublic.kt
注意:如果您使用 Detekt,这可能会与 Detekt 的 UnusedPrivateMember 规则冲突。请务必将 Detekt 的 ignoreAnnotated 配置设置为['预览'],以便与此规则兼容。
❞
为了实现开发者将逻辑和行为自由附加到 Compose UI 上的目的,Compose 推出了组合而非继承的理念。Modifier 则是实现这个理念的最重要组件。
Modifier 对所有公共的 UI 组件都很重要,通过它,调用者便可以按照自己的意愿定制组件的各种组合。
更多信息可以参考:
该 rule 的名称和源码:modifier-missing-check -> ComposeModifierMissing.kt
传入的 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 的名称和源码:modifier-reused-check -> ComposeModifierReused.kt
可将 Modifier 作为参数应用于所代表的整个组件的可组合函数,应命名该参数为 modifier,并分配 Modifier 参数的默认值。它应当声明为参数列表中的第 1 个可选参数,且位于所有必选参数(尾部的 lambda 参数除外)之后,但应位于任何其他具有默认值的参数之前。
在可组合函数的实现中,可组合函数所需的任何默认 Modifier 都应位于 Modifier 参数值之后,并将 Modifier
保留为默认参数值。
更多信息可以参考:
该 rule 的名称和源码:modifier-without-default-check -> ComposeModifierWithoutDefault.kt
不推荐在可组合函数里使用常用的扩展函数去构造 Modifier 实例,因为它们会导致不必要的重组。为避免该情况,推荐使用 Modifier.composed
,因为它会将重组限制在 Modifier 实例上,而不是针对整个函数 tree。
而且 Composed Modifier 可能在组合之外创建出来、跨组件之间共享、并声明为顶层常量,这使得它们比在可组合函数里调用扩展函数创建的 Modifier 更灵活,也更容易避免意外地跨组件共享状态数据。
更多信息可以参考:
如上的 rules 是 Twitter 使用 Compose 开发多年以来,不断结合官方文档和实战总结出来的宝贵经验。
如果想要使用该规则去检测代码是否合适,可以使用 ktlint
、Detekt
来导入规则和部署检查: