Jetpack Compose 架构分层

本页面简要介绍了组成 Jetpack Compose 的架构层,以及决定其设计的核心原则。

Jetpack Compose 不是一个单体式项目;它由一些模块构建而成,这些模块组合在一起,构成了一个完整的堆栈。通过了解组成 Jetpack Compose 的不同模块,您可以:

  • 使用适当的抽象级别来构建应用或库
  • 了解何时可以“降级”到较低级别,以获取更多的控制权或更高的自定义程度
  • 尽可能减少依赖项

Jetpack Compose 的主要层包括:

图 1. Jetpack Compose 的主要层

每一层都是基于较低的级别构建的,同时通过组合功能来创建更高级别的组件。每一层都是基于较低层的公共 API 构建的,用于验证模块边界,还支持您根据需要替换任何层。让我们自下而上地分析这些层。

运行时
此模块提供了 Compose 运行时的基本组件,例如 remembermutableStateOf@Composable 注解和 SideEffect。如果您只需要 Compose 的树管理功能,而不需要其界面,则可以考虑直接基于此层进行构建。
界面
界面层由多个模块(ui-textui-graphicsui-tooling 等)组成。这些模块实现了界面工具包的基本组件,例如 LayoutNodeModifier、输入处理程序、自定义布局和绘图。如果您只需要用到界面工具包的基本概念,则可以考虑基于此层进行构建。
基础
此模块为 Compose 界面提供了与系统无关的构建块,例如 RowColumnLazyColumn、特定手势的识别等。您可以考虑基于基础层构建自己的设计系统。
Material
此模块为 Compose 界面提供了 Material Design 系统的实现,同时提供了一个主题系统、样式化组件、指示器和图标。在您的应用中使用 Material Design 时,不妨基于此层进行构建。

设计原则

Jetpack Compose 的一个指导准则是提供可以组合在一起的重点突出的小块功能片段,而不是几个单体式组件。这种方法有许多优点。

控制

更高级别的组件往往能完成更多操作,但会限制您有多少直接控制权。如果您需要更多控制权,可以通过“降级”使用较低级别的组件。

例如,如果您想为某个组件的颜色添加动画效果,可以使用 animateColorAsState API:

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

不过,如果您之后需要这个组件始终从灰色开始,此 API 就无能为力了。您可以通过“降级”改用较低级别的 Animatable API:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

较高级别的 animateColorAsState API 本身基于较低级别的 Animatable API 构建而成。使用较低级别的 API 的过程更为复杂,但可提供更多的控制权。请选择最符合您需求的抽象化级别。

自定义

通过将较小的构建块组合成更高级别的组件,按需自定义组件的难度要小的多。例如,可以考虑使用 Material 层提供的 Button实现

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Button 由 4 个组件组合而成:

  1. Material Surface:用于提供背景、形状和点击处理方式等。

  2. CompositionLocalProvider:用于在启用或停用相应按钮时更改内容的 alpha 值

  3. ProvideTextStyle:用于设置要使用的默认文本样式

  4. Row:用于为相应按钮的内容提供默认布局政策

为了使结构更加清晰,我们省略了一些参数和注释,但整个组件只有 40 行左右的代码,因为它只是组合了这 4 个组件来实现该按钮。Button 等组件会自行判断它们需要公开哪些参数,同时在实现常见的自定义项和可能使组件更难使用的参数突增之间创造平衡。例如,Material 组件可提供 Material Design 系统中指定的自定义项,这样可以轻松遵循 Material Design 原则。

不过,如果您希望在组件的参数之外进行自定义,可以“降级”到某个级别并为组件创建分支。例如,Material Design 指定按钮应具有纯色背景。如果您需要渐变背景,Button 参数就不适用了,因为它不支持此选项。在此类情况下,您可以将 Material Button 实现用作参考,并构建您自己的组件:

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(/* … */)
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

上述实现继续使用 Material 层中的组件,例如 Material 的当前内容 alpha 值和当前文本样式的概念。不过,它会将 Material Surface 替换为 Row,并设置其样式以获得理想的外观。

如果您根本就不想使用 Material 概念,例如,在构建自己的定制设计系统时,可以降级为仅使用基础层组件:

@Composable
fun BespokeButton(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(/* … */)
            .background(/* … */)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose 为最高级别的组件保留了最为简洁的名称。例如,androidx.compose.material.Text 基于 androidx.compose.foundation.text.BasicText 构建。这样一来,如果您想替换更高级别,则可以为自己的实现提供更易于发现的名称。

选择合适的抽象化级别

Compose 构建可重复使用的分层组件的理念意味着您不应该始终以构建较低级别的构建块为目标。许多较高级别的组件不仅能够提供更多功能,而且通常还会实施支持无障碍功能等最佳做法。

例如,如果您想为自己的自定义组件添加手势支持,可以使用 Modifier.pointerInput 从头开始构建;但在此之上还有其他级别更高的组件,它们可以提供更好的起点,例如 Modifier.draggableModifier.scrollableModifier.swipeable

一般来讲,最好基于能提供您所需功能的最高级别的组件进行构建,以便从其包含的最佳做法中受益。

了解详情

如需查看有关构建自定义设计系统的示例,请参阅 Jetsnack 示例