虽然应用后台的工作非常重要, 但如何处理用户看到的内容对我们工作的成功至关重要. 有鉴于此, 我们要花大量时间考虑用户会做什么, 如何处理 UI 事件以及如何相应地构建代码. 在本文中, 我将介绍一种在 Jetpack Compose 中处理UI事件的简洁方法. 这是我从菲利普-莱克纳(Phillip Lackner)那里找到的方法, 我将对其进行扩展, 并解释如何使用它.
首先, 什么是UI事件? 简单地说, UI事件就是应由UI或 ViewModel 在UI层中处理的操作. 这些操作包括点击按钮, 在文本字段中输入文本, 显示错误信息或导航到另一个屏幕.
UI事件处理结构
我们处理 UI 事件的方法非常简单. 我们将有一个 UiEvent
接口, 一个 ViewModel
和一些 Composables. 用户与UI(Composable)交互, 然后触发 ViewModel
中的一个函数. 让我们看看处理这些UI事件的简洁方法.
简洁方法将包含三个组件:
- UiEvent -
UiEvent
是一个sealed interface
, 在这里, 我们将定义用户可以做的所有事情或在一个屏幕上发生的所有事情. - ViewModel - 在 ViewModel 中, 我们不需要为每次用户交互都设置一个单独的函数, 而是使用一个只有一个参数的函数:
onEvent(event: UiEvent)
. 该函数接收传递过来的UiEvent
事件, 并进行相应处理. - Composable - UI层. 在这里, 我们将调用
onEvent
函数并传入UiEvent
.
让我们通过代码来更好地理解这一点.
kotlin
sealed interface UiEvent {
data object InteractionOne: UiEvent
data class InteractionTwo(val value: String): UiEvent
}
在我们的 UiEvent
中, 我们定义了屏幕上可能发生的所有事件. 我们使用 data object
和 data class
来定义事件类型, 以表示我们的 UiEvents. 当我们需要从 Ui 向 ViewModel 传递数据(如文本输入)时, 就会使用数据类.
我们使用sealed interface
有两个原因. 首先, 一旦编译了带有sealed interface的模块, 就不能再创建新的实现. 这意味着我们定义的类是固定的, 以后不能再添加任何其他类. 其次, 使用sealed interface
和when
语句, 可以覆盖所有可能定义的 UiEvents 类型.
想了解更多有关 sealed interfaces
的信息, 请点击这里.
kotlin
class EventViewModel(
private val onNavigate: () -> Unit
): ViewModel() {
fun onEvent(event: UiEvent) {
when (event) {
UiEvent.InteractionOne -> {
println("This is event one")
}
is UiEvent.InteractionTwo -> {
println("This is event two and its value: ${event.value}")
}
}
}
}
在 EventViewModel
中, 我们只有一个名为 onEvent
的函数. 该函数的参数为 UiEvent
. 由于它是一个 sealed interface
, 因此将它与 when
结合使用可确保我们涵盖所有可能的 UiEvent
类型. 根据不同的类型, 将调用不同的函数.
kotlin
@Composable
fun HomeScreen(
state: EventUiState,
onEvent: (UiEvent) -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth()) {
Button(onClick = { onEvent(UiEvent.InteractionOne }) {
Text("Print InteractionOne")
}
Button(onClick = { onEvent(UiEvent.InteractionTwo("Hello"))}) {
Text("Print InteractionTwo")
}
}
}
在 HomeScreen
Composable 中, 我们只需传递一个函数. 由于 onEvent
接收一个 UiEvent
作为参数, 因此屏幕上的每次交互都将使用相同的函数, 只是使用不同的 UiEvents
.
如果我们运行代码并点击按钮, 就会看到我们的函数正在工作, 并按照预期打印出来.
好处
这看起来像是一些额外的步骤, 但让我们来看看它的好处.
- 分层
- 简化Composable程序
- 可扩展性和可维护性
我们将进一步深入探讨这些优点.
分层解耦
使用这种样式的目的是进一步分离我们的层. 以RadioButton
为例, 我们可以如下所示直接从UI中的ViewModel
调用函数, 这样就可以正常工作了.
scss
Button(onClick = { viewModel.printInteractionOne() }) {
Text("Print InteractionOne")
}
不过, 在这个例子中, UI与 ViewModel 有直接的交互. 一般来说, 在设计项目结构时, 我们会尽量将各层分开. 通过使用 UiEvent
, UI无需依赖 ViewModel 在交互过程中的操作.
简化Composable元素
在上面的示例中, 我们没有向Composable函数传递两个函数, 而是只传递了一个. 如果没有 onEvent
函数, 我们就必须分别传入每个 lambda 函数, 就像这样.
kotlin
@Composable
fun EventDemoButtons(
onInteractionOneClick: () -> Unit,
onInteractionTwoClick: (String) -> Unit,
) {
Button(onClick = { onInteractionOneClick() }) {
Text("Print InteractionOne")
}
Button(onClick = { onInteractionTwoClick("Hello") }) {
Text("Print InteractionTwo")
}
}
ini
EventDemoButons(
onInteractionOneClick = { viewModel.onInteractionOneClick() },
onInteractionTwoClick = { viewModel.onInteractionTwoClick(it) }
)
同样, 这样做也行得通, 但这只是一个有两个按钮的简单功能. 随着功能变得越来越复杂, UI事件的数量也会增加. 传递每个函数可能会导致混淆哪个函数做什么. 通过传递单个函数进行简化是更简洁的方法.
kotlin
@Composable
fun EventDemoButtons(
onEvent: (UiEvent) -> Unit
) {
Button(onClick = { onEvent(UiEvent.InteractionOne) }) {
Text("Print InteractionOne")
}
Button(onClick = { onEvent(UiEvent.InteractionTwo("Hello")) }) {
Text("Print InteractionTwo")
}
}
以下是调用 DemoButtonScreen
的方法.
ini
EventDemoButtons(onEvent = {
viewModel.onEvent(it)
})
可扩展性和可维护性
在开发功能的过程中, 总会有需要使用新功能, 修改交互或其他情况的时候. 使用集中式事件处理方法可以更轻松地处理这些变化. 例如, 如果我们需要添加新的交互InteractionThree
, 我们只需将其添加到UiEvent
接口, 然后用新的交互更新onEvent
即可.
kotlin
sealed interface UiEvent {
data object InteractionOne: UiEvent
data class InteractionTwo(val value: String): UiEvent
data object InteractionThree: UiEvent
}
kotlin
fun onEvent(event: UiEvent) {
when (event) {
UiEvent.InteractionOne -> {
Log.d("UiEvent", "This is event one")
}
is UiEvent.InteractionTwo -> {
Log.d("UiEvent", "This is event two and its value: ${event.value}")
}
UiEvent.InteractionThree -> {
Log.d("UiEvent", "This is event three")
}
}
}
示例
我们已经了解了如何以简洁的方式实现 UI 事件处理, 但有哪些实际例子呢? 让我们实现一个包含TextField
和Button
的基本屏幕.
ini
@Composable
fun HomeScreen(
state: EventUiState,
onEvent: (UiEvent) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.weight(0.5f))
TextField(modifier = Modifier.fillMaxWidth(),
value = state.text, onValueChange = {
onEvent(UiEvent.OnValueChange(it))
})
Button(modifier = Modifier.fillMaxWidth(), onClick = { onEvent(UiEvent.Save) }) {
Text(
text = "Save"
)
}
Spacer(modifier = Modifier.weight(0.5f))
}
}
kotlin
sealed interface UiEvent {
data class OnValueChange(val value: String): UiEvent
data object Save: UiEvent
}
vbnet
when (event) {
is UiEvent.OnValueChange -> {
_state.update {
it.copy(
text = event.value
)
}
}
UiEvent.Save -> {
onNavigate()
}
}
}
在这里, 我们将使用TextField
中的 UiEvent.OnValueChange()
更新ViewModel
中的状态并传入新值. 使用这种方法, UI不会知道数据发生了什么变化, 它与UI中发生的事情是完全分离的.
添加到示例中
哎呀, 我们没有办法在代码中处理错误. 如果TextField
为空怎么办? 让我们来实现一些错误处理. 我们可以在ViewModel
中添加一个私有函数来检查文本是否为空.
kotlin
private fun checkForErrors(): Boolean {
if (_state.value.text.isBlank()) {
_state.update {
it.copy(
errorMessage = "Text can't be blank."
)
}
}
return _state.value.errorMessage == null
}
现在我们可以修改在 onEvent
中处理 UiEvent.Save
的方式.
scss
UiEvent.Save -> {
if (checkForErrors()) {
onNavigate()
}
}
很好, 现在我们可以在文本字段为空时显示错误信息了. 我们还应将状态的 errorMessage
设置为空, 以确保用户稍后可以保存. 我们可以在 UiEvent
中添加一个名为 ClearError
的事件.
kotlin
sealed interface UiEvent {
data class OnValueChange(val value: String): UiEvent
data object Save: UiEvent
data object ClearError: UiEvent
}
将事件处理添加到 onEvent
.
ini
UiEvent.ClearError -> {
_state.update {
it.copy(
errorMessage = null
)
}
}
最后, 我们可以在Composable
中添加显示错误信息和调用UiEvent.ClearError
的逻辑. 同样, 我们只需将事件传入UI中的 onEvent
函数, 仅此而已.
scss
val context = LocalContext.current
LaunchedEffect(key1 = state.errorMessage) {
if (state.errorMessage != null) {
Toast.makeText(context, state.errorMessage, Toast.LENGTH_SHORT).show()
onEvent(UiEvent.ClearError)
}
}
总结一下
就是这样! 我们已经了解了如何以更简洁的方式处理UI事件, 方法是创建一个sealed interface来定义所有事件, 实现一个以事件为参数的 onEvent
函数, 最后只将 onEvent
函数传递给我们的 Composable
.
你如何处理 UI 事件? 请告诉我你的想法!