设计 Compose UI 架构

在 Compose 中,UI 是不可变的——绘制完成后无法更新它。您可以控制的是 UI 的状态。每当 UI 状态发生变化时,Compose 会重新创建 UI 树中已更改的部分。可组合项可以接受状态并公开事件——例如,一个 TextField 接受一个值并公开一个回调 onValueChange,该回调请求回调处理程序更改该值。

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

由于可组合项接受状态并公开事件,单向数据流模式非常适合 Jetpack Compose。本指南重点介绍如何在 Compose 中实现单向数据流模式,如何实现事件和状态持有者,以及如何在 Compose 中使用 ViewModel。

单向数据流

单向数据流 (UDF) 是一种设计模式,其中状态向下流动,事件向上流动。通过遵循单向数据流,您可以将 UI 中显示状态的可组合项与应用中存储和更改状态的部分解耦。

使用单向数据流的应用的 UI 更新循环如下所示:

  • 事件:UI 的一部分生成一个事件并将其向上传递,例如按钮点击传递给 ViewModel 处理;或者事件从应用的其他层传递过来,例如指示用户会话已过期。
  • 更新状态:事件处理程序可能会更改状态。
  • 显示状态:状态持有者向下传递状态,UI 显示它。

图 1. 单向数据流。

在使用 Jetpack Compose 时遵循此模式具有以下优点:

  • 可测试性:将状态与显示状态的 UI 解耦,使其更容易分别进行测试。
  • 状态封装:由于状态只能在一个地方更新,并且可组合项的状态只有一个真实来源,因此不太可能因状态不一致而产生错误。
  • UI 一致性:通过使用可观察状态持有者(如 StateFlowLiveData),所有状态更新会立即反映在 UI 中。

Jetpack Compose 中的单向数据流

可组合项基于状态和事件工作。例如,一个 TextField 仅在其 value 参数更新时才更新,并且它公开了一个 onValueChange 回调——一个请求将值更改为新值的事件。Compose 将 State 对象定义为一个值持有者,并且状态值的更改会触发重新组合。您可以将状态保存在 remember { mutableStateOf(value) }rememberSaveable { mutableStateOf(value) } 中,具体取决于您需要记住该值的时间。

TextField 可组合项的值类型是 String,因此它可以来自任何地方——硬编码值、ViewModel 或从父级可组合项传入。您不必将其保存在 State 对象中,但当调用 onValueChange 时,您需要更新该值。

定义可组合参数

在定义可组合项的状态参数时,您应该记住以下问题:

  • 可组合项的可重用性或灵活性如何?
  • 状态参数如何影响此可组合项的性能?

为了鼓励解耦和重用,每个可组合项应尽可能少地包含信息。例如,在构建用于新闻文章标题的可组合项时,最好只传入需要显示的信息,而不是整篇新闻文章:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

有时,使用单个参数也能提高性能——例如,如果 News 包含的信息不仅仅是 titlesubtitle,那么每当新的 News 实例传入 Header(news) 时,即使 titlesubtitle 没有改变,可组合项也会重新组合。

仔细考虑您传入的参数数量。参数过多的函数会降低函数的易用性,因此在这种情况下,最好将它们分组到一个类中。

Compose 中的事件

应用的所有输入都应表示为事件:轻触、文本更改,甚至计时器或其他更新。当这些事件更改 UI 的状态时,ViewModel 应负责处理它们并更新 UI 状态。

UI 层绝不应在事件处理程序之外更改状态,因为这可能会在您的应用中引入不一致和错误。

优先传递不可变的状态值和事件处理程序 lambda。这种方法具有以下优点:

  • 您提高了可重用性。
  • 您确保您的 UI 不会直接更改状态的值。
  • 您避免了并发问题,因为您确保状态不会从另一个线程中被修改。
  • 通常,您会降低代码复杂性。

例如,一个接受 String 和 lambda 作为参数的可组合项可以在许多上下文中调用,并且具有高度可重用性。假设您应用中的顶部应用栏始终显示文本并有一个返回按钮。您可以定义一个更通用的 MyAppTopAppBar 可组合项,它接收文本和返回按钮处理程序作为参数:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModels、状态和事件:一个例子

通过使用 ViewModelmutableStateOf,如果以下任一条件成立,您也可以在应用中引入单向数据流:

  • 您的 UI 状态通过可观察状态持有者(如 StateFlowLiveData)公开。
  • ViewModel 处理来自 UI 或应用其他层的事件,并根据事件更新状态持有者。

例如,在实现登录屏幕时,点击“登录”按钮应使您的应用显示一个进度微调器并进行网络调用。如果登录成功,则您的应用会导航到不同的屏幕;如果出现错误,应用会显示一个 Snackbar。以下是您如何建模屏幕状态和事件:

屏幕有四种状态:

  • 已退出登录:当用户尚未登录时。
  • 进行中:当您的应用当前正在通过执行网络调用尝试登录用户时。
  • 错误:登录时发生错误。
  • 已登录:当用户已登录时。

您可以将这些状态建模为密封类。ViewModel 将状态作为 State 公开,设置初始状态,并根据需要更新状态。ViewModel 还通过公开 onSignIn() 方法来处理登录事件。

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

除了 mutableStateOf API,Compose 还提供扩展,用于 LiveDataFlowObservable 以注册为监听器并表示值为状态。

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

了解更多

要了解有关 Jetpack Compose 架构的更多信息,请查阅以下资源:

示例