在 Compose 中,UI 是不可变的——一旦绘制完成,就无法更新它。你可以控制的是 UI 的状态。每次 UI 状态发生变化时,Compose 都会 重新创建 UI 树中发生变化的部分。Composable 可以接受状态并公开事件——例如,一个 TextField
接受一个值,并公开一个回调 onValueChange
,该回调请求回调处理程序更改该值。
var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } )
由于 composable 接受状态并公开事件,因此单向数据流模式非常适合 Jetpack Compose。本指南重点介绍如何在 Compose 中实现单向数据流模式,如何实现事件和状态持有者,以及如何在 Compose 中使用 ViewModel。
单向数据流
单向数据流 (UDF) 是一种设计模式,其中状态向下流动,事件向上流动。通过遵循单向数据流,你可以将显示 UI 中状态的 composable 与存储和更改状态的应用程序部分解耦。
使用单向数据流的应用程序的 UI 更新循环如下所示
- 事件:UI 的一部分生成事件并将其向上传递,例如传递给 ViewModel 以处理的按钮点击;或从应用程序的其他层传递的事件,例如指示用户会话已过期。
- 更新状态:事件处理程序可能会更改状态。
- 显示状态:状态持有者向下传递状态,UI 显示它。
在使用 Jetpack Compose 时遵循此模式可以提供以下几个优势
- 可测试性:将状态与显示它的 UI 解耦,使两者更容易独立测试。
- 状态封装:由于状态只能在一个地方更新,并且 composable 的状态只有一个真相来源,因此由于状态不一致而导致错误的可能性降低。
- UI 一致性:通过使用可观察的状态持有者(如
StateFlow
或LiveData
),所有状态更新都会立即反映在 UI 中。
Jetpack Compose 中的单向数据流
Composable 基于状态和事件工作。例如,一个 TextField
只有在其 value
参数更新时才会更新,并且它公开一个 onValueChange
回调——一个请求将值更改为新值的事件。Compose 将 State
对象定义为值持有者,而状态值的变化会触发重新组合。你可以将状态保存在 remember { mutableStateOf(value) }
或 rememberSaveable { mutableStateOf(value)
中,具体取决于你需要记住该值的时长。
TextField
composable 的值类型为 String
,因此它可以来自任何地方——来自硬编码值、来自 ViewModel 或从父 composable 传递进来。你不必将它保存在 State
对象中,但你需要在调用 onValueChange
时更新该值。
定义 composable 参数
定义 composable 的状态参数时,你应该牢记以下问题
- composable 的可重用性或灵活性如何?
- 状态参数如何影响此 composable 的性能?
为了鼓励解耦和重用,每个 composable 应尽可能少地持有信息。例如,在构建 composable 以容纳新闻文章的标题时,建议只传递需要显示的信息,而不是整个新闻文章
@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
包含的信息不只是 title
和 subtitle
,那么每次将 News
的新实例传递到 Header(news)
时,composable 都会重新组合,即使 title
和 subtitle
没有更改。
仔细考虑传递的参数数量。函数的参数过多会降低函数的人体工程学,因此在这种情况下,建议将它们分组到一个类中。
Compose 中的事件
应用程序的每个输入都应表示为事件:点击、文本更改,甚至计时器或其他更新。当这些事件改变 UI 的状态时,ViewModel
应该负责处理它们并更新 UI 状态。
UI 层绝不应该在事件处理程序之外更改状态,因为这会导致应用程序中出现不一致和错误。
建议为状态和事件处理程序 lambda 传递不可变的值。这种方法具有以下优点
- 你提高了可重用性。
- 你确保 UI 不会直接更改状态的值。
- 你避免了并发问题,因为你确保状态不会从另一个线程突变。
- 通常,你降低了代码复杂度。
例如,接受 String
和 lambda 作为参数的 composable 可以从许多上下文中调用,并且具有很高的可重用性。假设应用程序中的顶部应用栏始终显示文本并具有后退按钮。你可以定义一个更通用的 MyAppTopAppBar
composable,它接收文本和后退按钮处理程序作为参数
@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.Filled.ArrowBack, contentDescription = localizedString ) } }, // ... ) }
ViewModel、状态和事件:一个示例
通过使用 ViewModel
和 mutableStateOf
,你也可以在应用程序中引入单向数据流,如果满足以下条件之一
- UI 的状态通过可观察的状态持有者公开,例如
StateFlow
或LiveData
。 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 还为 LiveData
、Flow
和 Observable
提供扩展 以注册为侦听器并将值表示为状态。
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 中的架构设计,请参阅以下资源
示例
推荐给您
- 注意:当 JavaScript 关闭时,链接文本将显示
- 状态和 Jetpack Compose
- 在 Compose 中保存 UI 状态
- 处理用户输入