更多翻译好文:

  • [译]Compose之解密ViewCompositionStrategy – ()

文章作者:Ben Trengrove

原文链接:Jetpack Compose Stability Explained | by Ben Trengrove | Android Developers | Medium

️全部版权归作者一切,本译文仅用于技能交流请勿用于商业用途,未经答应禁止转载,违者后果自负


你是否从前测量过可组合项的功能并发现它重组的次数比你预期的要多?你或许会问:“莫非Compose的含义不就是状况没有产生变化的时分智能地越过那些重组吗”。或许在阅读代码时,你或许会看到运用了@Stable或许@Immutable注释的类,而且想知道这是什么意思?这些概念都能够运用Compose的安稳性(Stability)来解释。在这篇博文中,咱们将了解Compose安稳性的实践含义、怎么调试它以及你是否应该忧虑它。

摘要

  • Compose 查看可组合项的每个参数的安稳性,以承认在重组期间是否能够越过它。
  • 假如你留意到你的可组合项没有被越过而且它导致了功能问题,你应该首要查看不安稳的显着原因,例如 var 参数。
  • 你能够运用编译器陈述来承认所揣度的关于你的类的安稳性。
  • ListSetMap 这样的调集类总是被承认为不安稳的,由于不能确保它们是不行变的。你能够改用 Kotlinx 不行变调集,或将你的类注释为 @Immutable@Stable
  • 来自未运转 Compose 编译器的module的类一直被承认为不安稳。增加 compose 运转时的依靠,并在你的模块中将它们标记为安稳,或根据需求将类包装在 UI model类中
  • 每个可组合项都应该是可越过的吗?不。

什么是重组(recomposition)?

在评论安稳性之前,让咱们快速回忆一下重组的界说:

重组是当入参产生变化时再次调用可组合函数的进程。当函数的入参产生时,就会产生这种状况。当Compose根据新输入进行重组(recomposition)时,它只会调用或许已更改的函数或lambda,并越过其余部分。通过越过一切没有更改参数的函数或 lambda,Compose 能够高效地重组。

留意那里的关键词——“或许”。 Compose 将在快照状况更改时触发重组,并越过任何未更改的可组合项。重要的是,只有当 Compose 能够承认可组合项的一切参数都没有更新时,才会越过可组合项。不然,假如 Compose 不能承认一切参数都没有更新时,它总是会在其父可组合项被重组时被重组。假如 Compose 不这样做,或许会导致不能正确触发重组的过错。正确但功能稍差比不正确但功能稍快要好得多。

让咱们运用一个显现联系人具体信息的Row示例:

fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
    var selected by remember { mutableStateOf(false) }
    Row(modifier) {
     ContactDetails(contact)
     ToggleButton(selected, onToggled = { selected = !selected })
    }
}

运用不行变(immutable)目标

首要,假设咱们将 Contact 类界说为不行变数据类,因而假如不创立新目标就无法更改它的数值:

data class Contact(val name: String, val number: String)

单击ToggleButton按钮时,咱们会更改挑选状况。这会触发 Compose 评价是否应重构 ContactRow 中的代码。当涉及到 ContactDetails 可组合项时,Compose 将越过从头组合它。这是由于它能够看到没有任何参数(在本例中为联系人)产生变化。另一方面,ToggleButton 的输入已更改,因而能够正确重组。

运用可变(mutable)目标

假如咱们的 Contact 类是这样界说的呢?

data class Contact(var name: String, var number: String)

现在咱们的 Contact 类不再是不行变的,它的属功能够在 Compose 不知道的状况下改动。 Compose 将不再越过 ContactDetails 可组合项,由于该类现在被视为“不安稳”(下文将具体介绍这意味着什么)。因而,只要所选内容产生更改,ContactRow 也将从头组合。

Compose 编译器中的完成

现在咱们知道了Compose企图在承认什么(译者:指的是目标的可变性),让咱们看看这实践是怎么完成的。

首要,这是Compose 文档 (1, 2) 中的一些界说。

办法(Functions)能够可越过(skippable)或许可重启(restartable):

可越过(Skippable)——在重组期间调用时,假如一切参数都等于它们之前的值,则 Compose 能够越过该函数。 。

可重启(Restartable)——此函数能够作为重组作用域(换句话说,此函数能够用作 Compose 能够在状况更改后开始从头履行代码以进行重组的入口点)。

类型能够是不行变(Immutable)的或许安稳(Stable)的

不行变——表明一种类型,其间任何特点的值在结构目标后都不会改动,而且一切办法都是引证透明的。一切底子类型(StringIntFloat 等)都被认为是不行变的。

安稳——表明一种类型是可变的,但假如任何公共特点或办法行为会产生与从前调用不同的结果,Compose 运转时将收到通知(译者:尽管目标内部的数值尽管会产生变化,可是这种变化能够被Compose辨认)。

当 Compose 编译器在你的代码的编译阶段时,它会查看每个函数和类型并标记任何与这些界说匹配的函数和类型。 Compose 查看传递给可组合项的类型以承认该可组合项的可越过性(skippability)。重要的是要留意参数不必是不行变(Immutable)的,只要将一切更改通知 Compose 运转时,它们就能够是可变的(译者:即类也能够是安稳的)。关于大多数类型来说,这将是一个没什么含义的约好,可是 Compose 供给了可变类来为你保护这个约好,例如 MutableStateSnapshotStateMap/List/等。因而,将这些类型用于可变特点将答应您的类保护 @Stable 的契约。在实践中,这看起来像下面这样:

    @Stable
    class MyStateHolder {
     var isLoading by mutableStateOf(false)
    }

当Compose状况变化时,Compose会在树中读取这些状况目标的点上寻觅最近的可组合函数。理想状况下,这将是从头运转尽或许小的代码的直接先人。正由于这样,重组重启时,假如参数未改动,任何可越过的函数都将被越过。让咱们从头看看之前的比如:

  data class Contact(val name: String, val number: String)
  fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
     var selected by remember { mutableStateOf(false) }
     Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
      }
 }

代码中,当 selected 产生变化时,距离被读取的状况(stable)最近的重组作用域是ContactRow。你或许想知道为什么 Row 没有被选为最近的重组作用域?Row(以及许多其他基础可组合项,如 ColumnBox)实践上是一个内联函数(inline function),内联函数不是重组作用域,由于它们在编译后实践上并没有最终成为函数。 因而ContactRow顺位成为最小的重组规模。由于Contact被揣度为不行变,所以ContactDetails被标记为可越过,Compose编译器增加的代码会查看任何可组合项参数已更改。

contact坚持不变时,ContactDetails会越过重组。接下来,点击ToggleButton,尽管ToggleButton是能够被越过的,可是这种状况下就不会被越过了,由于其间一个参数,selected已经改动了,因而会导致ToggleButton被从头履行。这会整个重组作用域被从头履行,完成了一次重组。

重组图解:miro.medium.com/v2/resize:f…

你或许会觉得,“这真的很杂乱!为什么我需求知道这个?!”答案是,大多数时分你不应该这样做,咱们的目标是让编译器优化您自然编写的代码以提高功率。越过可组合函数是完成这一点的重要因素,但它也需求 100% 安全,不然会导致很难承认的bug。为此,对要越过的可组合项的要求是很强的。咱们正在尽力改善编译器对可越过性的揣度,但总会有编译器无法处理的状况。了解在这种状况下越过可组合项的作业原理能够帮助您提高功能,但只有在您遇到由安稳性(stability)引起的显着的功能问题时才应考虑。假如可组合项是轻量级的或本身仅包含可越过的可组合项,则不行越过的可组合项或许底子没有任何作用。(译者:假如不是遇到了很严重的功能问题,或许可组合项很轻量,则不必考虑安稳性带来的问题)

调试安稳性

怎么知道你的可组合项是否被越过?你能够在Layout Inspector中看到它! Android Studio Dolphin 在 Layout Inspector 中包含对 Compose 的支撑,它还会显现您的可组合项被重组和越过的次数。

[译]  Compose之稳定性(Stability)的解释

Layout Inspector中的重组次数

那么,假如你看到你的可组合项没有被越过,即便它的参数都没有改动,您会怎么做?最简单的办法是查看它的界说,看看它的任何参数是否显着可变。你是否传递了具有 var 特点或 val 特点但具有已知不安稳类型的类型?假如是,那么该可组合项将永远不会被越过!

可是,当你无法发现任何显着过错时,你会怎么做?

Compose编译器陈述

Compose编译器能够输出其安稳性揣度的结果以供查看。结合查看陈述,你能够承认哪些可组合项是可越过的,哪些不是。这篇文章总结了怎么运用这些陈述,但有关这些陈述的具体信息,请参阅 技能文档。

⚠️ 正告:只有当你确实遇到与安稳性相关的功能问题时,才应运用此技能。企图让你的整个 UI 都能够越过是过早优化,或许会导致未来的保护困难。在针对安稳性进行优化之前,请确保你遵循咱们关于 Compose 功能的 最佳实践。

默认状况下不启用编译器陈述。通过运用compiler flag来敞开Compose编译器陈述,具体设置因项目而异,但关于大多数项目,你能够将以下脚本粘贴到根 build.gradle 文件中。

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */subprojects {
 tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
 kotlinOptions {
  if (project.findProperty("composeCompilerReports") == "true") {
   freeCompilerArgs += [    "-P",    "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +    project.buildDir.absolutePath + "/compose_compiler"     ]
    }
   if (project.findProperty("composeCompilerMetrics") == "true") {
    freeCompilerArgs += [     "-P",     "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +     project.buildDir.absolutePath + "/compose_compiler"     ]
    }
   }
  }
}

要调试可组合项的安稳性,你能够运转以下task:

./gradlew assembleRelease -PcomposeCompilerReports=true

⚠️ 正告:确保一直在发布版本(release build )上运转它以确保精确的结果。

此使命将输出三个文件。 (包含来自Jetsnack的示例输出)

-classes.txt — 关于此模块中类的安稳性的陈述。 事例。

-composables.txt — 关于此模块中可组合项的可重启性和可越过性的陈述。事例.

-composables.csv — 上述文本文件的 csv 版本,用于导入电子表格或通过脚本处理。 事例.

假如你改为运转 composeCompilerMetrics 使命,你将获得项目中可组合项数量的总体统计信息和其他相似信息。这在这篇文章中没有涉及,由于它对调试没有那么有用。 翻开 composables.txt 文件,你将看到该模块的一切可组合函数,而且每个函数都将标记它们是否可从头启动、可越过及其参数的安稳性。这是来自Jetsnack 的一个假设示例,它是 Compose 示例运用程序之一。

restartable skippable scheme(“[androidx.compose.ui.UiComposable]”) fun SnackCollection(
  stable snackCollection: SnackCollection
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
  stable index: Int = @static 0
  stable highlight: Boolean = @static true
)

SnackCollection 可组合项彻底可重启可越过安稳。在或许的状况下,这通常是你想要的,尽管远非强制性的(博文末尾有更多具体信息)。

可是,让咱们看另一个比如。

restartable scheme(“[androidx.compose.ui.UiComposable]”) fun HighlightedSnacks(
  stable index: Int
  unstable snacks: List<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

HighlightedSnacks 可组合项是不行越过的——只要在重组期间调用它,即便它的参数都没有改动,它也会重组。 这是由不安稳的参数snacks引起的。

现在咱们来到 classes.txt 文件来查看 Snack 的安稳性。

unstable class Snack {
   stable val id: Long
   stable val name: String
   stable val imageUrl: String
   stable val price: Long
   stable val tagline: String
   unstable val tags: Set<String>
   <runtime stability> = Unstable
}

作为参考,这是 Snack 的声明方式

data class Snack(
    val id: Long,
    val name: String,
    val imageUrl: String,
    val price: Long,
    val tagline: String = "",
    val tags: Set<String> = emptySet()
)

snacks不安稳的。它的绝大部分参数都是安稳的,但tags被认为是不安稳的。但这是为什么呢? Set 看起来是不行变的,它不是 MutableSet。 不幸的是,Set(以及 List 和其他标准调集类,稍后会具体介绍)在 Kotlin 中被界说为接口,这意味着底层完成或许仍然是可变的。例如,你能够写:

val set: Set<String> = mutableSetOf(“foo”)

变量是常量,它声明的类型不是可变的,但它的完成仍然是可变的。 Compose 编译器无法承认此类的不变性,由于它只看到声明的类型,因而将其声明为不安稳的。现在让咱们看看怎么使它安稳。

让不安稳安稳(Stabilizing the unstable)

不安稳类导致了功能问题时,测验使其安稳是个好主意。首要要测验的是让类彻底不行变。

不行变——表明一种类型,其间任何特点的值在结构目标后都不会改动,而且一切办法都是引证透明的。一切底子类型(StringIntFloat 等)都被认为是不行变的。

换句话说,将一切 var 特点设为 val,并将一切这些特点设为不行变类型。

假如你无法完成上述要求,那你将不得不对任何可变特点运用 Compose State。

安稳——表明一种类型是可变的,但假如任何公共特点或办法行为会产生与从前调用不同的结果,Compose 运转时将收到通知(译者:尽管目标内部的数值尽管会产生变化,可是这种变化能够被Compose辨认)。

这意味着在实践中,任何可变特点都应该由 Compose 状况支撑,例如 mutableStateOf(…)

回到 Snack 示例,该类看起来是不行变的,那么咱们怎么处理它呢?

你能够采取以下办法:

Kotlinx 不行变调集(Immutable Collections)

Compose 编译器的 1.2 版包含对 Kotlinx Immutable Collections的支撑。这些调集确保是不行变的,而且将由编译器揣度为不行变的。该库仍处于 alpha 阶段,因而预计其 API 或许会产生变化。你应该评价这对你的项目是否能够承受。

tags的类型改动为下面这种类型能够让Snack安稳

val tags: ImmutableSet<String> = persistentSetOf()

运用Stable或许Immutable注释

根据上述规则,类也能够运用 @Stable 或 @Immutable 进行注释。

⚠️ 正告:非常需求留意的是,这是一个约好,要遵循相应的注解规则。它本身不会使类不行变/安稳。过错地运用注释或许会导致重组失利。

注释一个类会覆盖编译器对你的类的揣度,这样它相似于kotlin的!!运算符。你应该非常当心这些注释的运用,由于假如你弄错了,覆盖编译器行为或许会导致你出现无法意料的过错。假如能够在没有注释的状况下使您的类安稳,那么你应该尽力以这种方式完成安稳。

正确注释Snack的方式如下:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */@Immutable
data class Snack(
 val id: Long,
 val name: String,
 val imageUrl: String,
 val price: Long,
 val tagline: String = "",
 val tags: Set<String> = emptySet()
)

不管挑选哪种办法,Snack 类都将被揣度为安稳的。

可是,回到 HighlightedSnacks 可组合项,HighlightedSnacks 仍未标记为可越过:

unstable snacks: List<Snack>

当涉及到调集类型时,参数面对与类相同的问题,List 总是被承认为不安稳的,即便它是安稳类型的调集。 你也不能将单个参数标记为安稳,也不能将可组合项注释为一直可越过。所以,你能够做什么?相同,也有许多种办法处理这个问题。

运用Kotlinx 不行变调集而不是List

 /* Copyright 2022 Google LLC.
 SPDX-License-Identifier: Apache-2.0 */@Composable
 private fun HighlightedSnacks(
  index: Int,
    snacks: ImmutableList<Snack>,
  onSnackClick: (Long) -> Unit,
  modifier: Modifier = Modifier
  )

假如你不能运用不行变调集,你能够在最简单的状况下将List包装在带注释的安稳类中,以将其标记为对 Compose 编译器不行变。

    @Immutable
    data class SnackCollection(
     val snacks: List<Snack>
    )

然后,你能够将其用作可组合项中的参数类型。

     @Composable
     private fun HighlightedSnacks(
      index: Int,
        snacks: SnackCollection,
      onSnackClick: (Long) -> Unit,
      modifier: Modifier = Modifier
      )

在采用其间任何一种办法后,HighlightedSnacks 可组合项现在既能够越过也能够从头启动。

restartable skippable scheme(“[androidx.compose.ui.UiComposable]”) fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

HighlightedSnacks 现在将在其入参均未更改时越过重组。

多模块

你或许遇到的另一个常见问题与多模块架构有关。 Compose 编译器揣度一个类是否安稳,前提是它引证的一切非原始类型都被显式标记为安稳的,而且坐落Compose 编译器构建的模块中。假如你的数据层(data layer)和UI层(UI layer,)是分隔的(这是引荐的办法),这或许是你会遇到的问题。要处理此问题,你能够:

  • 在你的数据层模块上启用 Compose 编译器,或在恰当的地方运用 @Stable 或 @Immutable 标记你的类。
  • 这将涉及向数据层增加 Compose 依靠项,你只需求增加Compose运转时的依靠而不用增加Compose-UI依靠。
  • 将你的数据层的类包装在你的UI层的特定包装类中。

相同的问题也会产生在外部module上,除非它们运用的是 Compose 编译器。

这是一个已知的限制,咱们目前正在研讨针对多模块架构和外部库的更优点理方案。

一切的重组都应该被越过吗?

不。

寻求运用中每个可组合项的彻底可越过性是不成熟的优化。可越过实践上会增加其本身的少量开支,这或许不值得,假如你承认可重启的开支大于其价值,你甚至能够将可组合项注释为 不行重启。在许多其他状况下,可越过不会有任何实践优点,只会导致难以保护代码。例如:

  • 不常常重组或底子不重组的可组合项。
  • 只是被称为可越过可是实践上项目中没有越过的场景的可组合项。

总结

这篇博文中有许多信息,所以让咱们总结一下。

  • Compose 查看可组合项的每个参数的安稳性,以承认在重组期间是否能够越过它。
  • 假如你留意到你的可组合项没有被越过而且它导致了功能问题,你应该首要查看不安稳的显着原因,例如 var 参数。
  • 你能够运用编译器陈述来承认所揣度的关于你的类的安稳性。
  • ListSet 和 Map 这样的调集类总是被承认为不安稳的,由于不能确保它们是不行变的。你能够改用 Kotlinx 不行变调集,或将你的类注释为 @Immutable@Stable
  • 来自未运转 Compose 编译器的module的类一直被承认为不安稳。增加 compose 运转时的依靠,并在你的模块中将它们标记为安稳,或根据需求将类包装在 UI model类中
  • 每个可组合项都应该是可越过的吗?不。

有关 Compose 功能的更多调试技巧,请查看咱们的最佳实践指南 和 I/O talk。