如何在 Jetpack Compose 中简洁地处理 UI事件?

虽然应用后台的工作非常重要, 但如何处理用户看到的内容对我们工作的成功至关重要. 有鉴于此, 我们要花大量时间考虑用户会做什么, 如何处理 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 objectdata class 来定义事件类型, 以表示我们的 UiEvents. 当我们需要从 Ui 向 ViewModel 传递数据(如文本输入)时, 就会使用数据类.

我们使用sealed interface有两个原因. 首先, 一旦编译了带有sealed interface的模块, 就不能再创建新的实现. 这意味着我们定义的类是固定的, 以后不能再添加任何其他类. 其次, 使用sealed interfacewhen语句, 可以覆盖所有可能定义的 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 事件处理, 但有哪些实际例子呢? 让我们实现一个包含TextFieldButton的基本屏幕.

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 事件? 请告诉我你的想法!

相关推荐
guoruijun_2012_42 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood2 小时前
一文了解Android中的AudioFlinger
android·音频
B.-4 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
有趣的杰克4 小时前
Flutter【04】高性能表单架构设计
android·flutter·dart
大耳猫10 小时前
主动测量View的宽高
android·ui
帅次12 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛13 小时前
Android中Crash Debug技巧
android
kim565918 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼18 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ18 小时前
Android Studio使用c++编写
android·c++