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

相关推荐
CYRUS_STUDIO1 小时前
利用 Linux 信号机制(SIGTRAP)实现 Android 下的反调试
android·安全·逆向
CYRUS_STUDIO1 小时前
Android 反调试攻防实战:多重检测手段解析与内核级绕过方案
android·操作系统·逆向
黄林晴5 小时前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我5 小时前
flutter 之真手势冲突处理
android·flutter
天花板之恋6 小时前
Compose之图片加载显示
android jetpack
法的空间6 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止6 小时前
深入解析安卓 Handle 机制
android
恋猫de小郭6 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech6 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831676 小时前
为何Handler的postDelayed不适合精准定时任务?
android