因为AI,所以deepseek
以下是翻译内容:
在不崩溃的前提下预览你的 KMP 驱动的 Compose UI
大多数 Android 开发者与 Jetpack Compose 预览的关系都很复杂。它们慢,它们坏,它们在复杂应用中不起作用。这些是你常听到的抱怨,然后开发者就放弃了,又回到每次花三十秒构建和运行来检查一个按钮是否居中的日子。
坦率地说,这通常是他们(我也是)自己的错。
预览不起作用,是因为他们试图预览的 UI 是一个业务逻辑、状态计算和隐式依赖纠缠在一起的混乱体。当你的 @Composable 函数还要负责决定获取什么数据以及如何格式化时,你写的就不是一个 UI 组件。你写的是一个微型的、不可测试的应用程序。不,预览工具无法运行你的整个应用程序。
在 Kotlin 多平台架构中,Android 应用是一个专门的 UI 层,这些借口就烟消云散了。
如果你的预览仍然崩溃,问题不在于工具,而在于你的自律性。
基础:为何这应该很简单
让我们明确它是如何工作的。一个 @Composable 函数,本质上是一个将状态转换为 UI 的函数。Compose 运行时在这方面非常高效,但它依赖于一个简单的契约:UI = f(状态)。当你直接在组合函数中嵌入副作用和业务逻辑,你就破坏了这个模型。
我们的 KMP 设置给了我们一个良好的开端。所有困难的部分——状态管理、业务逻辑、数据获取——都在共享模块中处理。KMP ViewModel 暴露状态,Android UI 层只是观察它。UI 的唯一工作是向 ViewModel 发送事件。实际上,它就是一个哑终端。
这是预览的完美环境。一个哑 UI 很容易预览,因为你可以提供你想要的任何状态,它会忠实地渲染它。
方法:一个简单、可重复的工作流程 为了让这个模式可靠地工作,我们为每个功能屏幕遵循一个严格的三部分模式。
<功能>Screen:这是“智能”组合函数。它是该功能中唯一允许与 KMP ViewModel 对话的组件。它的职责是从 ViewModel 收集状态(如果需要的话,将其映射到一个简单的 UI 特定状态模型),并将用户事件(点击、文本更改)连接到 ViewModel 的事件处理器。<功能>Content:这是“哑”组合函数。它包含该功能的所有 UI——Scaffold、Column、TextFields 和 Buttons。它对 ViewModel 或 KMP 一无所知。它接受两个参数:一个状态对象和一组用于事件的 lambda。这正是我们要预览的组件。<功能>UiStateProvider:这是一个简单的工厂类,实现PreviewParameterProvider。它的唯一目的是为每个我们想要可视化的可能场景生成 UI 状态的实例:加载中、成功、错误以及其他任何特定的业务用例。
让我们将此应用于一个虚构的 WCDonalds 应用,重点关注登录和注册流程。
首先,我们需要自定义预览注解以保持一致性。
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.ui.tooling.preview.Preview
@Preview(
name = "浅色模式",
group = "浅色",
showBackground = true,
backgroundColor = 0xffEEEEEE,
uiMode = UI_MODE_NIGHT_NO,
fontScale = 1f,
)
@Preview(
name = "深色模式",
group = "深色",
showBackground = true,
backgroundColor = 0xff444444,
uiMode = UI_MODE_NIGHT_YES,
fontScale = 1f,
)
@Preview(
name = "浅色模式 - 大字体",
group = "浅色",
showBackground = true,
backgroundColor = 0xffEEEEEE,
uiMode = UI_MODE_NIGHT_NO,
fontScale = 1.5f,
)
@Preview(
name = "深色模式 - 大字体",
group = "深色",
showBackground = true,
backgroundColor = 0xff444444,
uiMode = UI_MODE_NIGHT_YES,
fontScale = 1.5f,
)
annotation class WcdPreview
现在,让我们构建登录屏幕。 KMP 模块提供了一个表示状态的密封类。
// 由 KMP 共享模块提供
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Error(val message: String) : LoginState()
object TwoFactorRequired : LoginState()
object Success : LoginState()
}
我们的 LoginUiStateProvider 将为预览创建这个状态的实例。
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class LoginUiStateProvider : PreviewParameterProvider<LoginState> {
override val values: Sequence<LoginState> = sequenceOf(
LoginState.Idle,
LoginState.Loading,
LoginState.Error("用户名或密码无效。"),
LoginState.TwoFactorRequired
)
}
接下来,是 LoginContent 组合函数。它是无状态且哑的。它接收 LoginState 和用于每个可能用户交互的 lambda。
@Composable
fun LoginContent(
state: LoginState,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLoginClick: () -> Unit,
onTwoFactorCodeChange: (String) -> Unit,
onTwoFactorSubmit: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// ... 邮箱和密码的 TextFields ...
when (state) {
is LoginState.Idle -> {
Button(onClick = onLoginClick) { Text("登录") }
}
is LoginState.Loading -> {
CircularProgressIndicator()
}
is LoginState.Error -> {
Button(onClick = onLoginClick) { Text("登录") }
Text(text = state.message, color = MaterialTheme.colors.error)
}
is LoginState.TwoFactorRequired -> {
// ... 2FA 验证码的 TextField ...
Button(onClick = onTwoFactorSubmit) { Text("提交验证码") }
}
is LoginState.Success -> {
// 通常在此导航离开,但对于预览,我们可以显示成功信息。
Text("登录成功!")
}
}
}
}
哑的 LoginContent 准备就绪后,我们可以创建预览。
@WcdPreview
@Composable
fun LoginPreview(
@PreviewParameter(LoginUiStateProvider::class) state: LoginState
) {
WcdonaldsTheme { // 你的应用主题
LoginContent(
state = state,
onEmailChange = {},
onPasswordChange = {},
onLoginClick = {},
onTwoFactorCodeChange = {},
onTwoFactorSubmit = {}
)
}
}
最后,LoginScreen 在实际应用中连接一切。
@Composable
fun LoginScreen(
viewModel: LoginViewModel // 来自 KMP
) {
val state by viewModel.state.collectAsState()
LoginContent(
state = state,
onEmailChange = viewModel::onEmailChanged,
onPasswordChange = viewModel::onPasswordChanged,
onLoginClick = viewModel::onLoginClicked,
onTwoFactorCodeChange = viewModel::onTwoFactorCodeChanged,
onTwoFactorSubmit = viewModel::onTwoFactorSubmit
)
}
这个模式对任何组件都是可重复的。
需要预览一个处于禁用状态的按钮?创建一个 ButtonPreview,用 enabled = false 调用你的 WcdButton 组合函数。
原理相同:提供状态,渲染 UI。
好处,直截了当地说 遵循这个工作流程不仅仅是为了让预览工作。它强制推行了更清晰的架构。
- 更快的迭代:你可以构建整个 UI 流程,包括所有错误和边界情况,而无需运行应用程序。你可以开发 2FA 验证屏幕,而无需先实际登录。
- 架构纯粹性:UI 层保持简单。它不与逻辑纠缠。这符合 SOLID 原则,并保持代码 DRY(不重复)。
- 更容易上手:新开发者可以打开预览文件,立即了解屏幕可能处于的所有视觉状态,而无需了解底层业务逻辑的任何知识。
- 视觉测试:你可以立即发现设计系统在不同状态(浅色/深色模式、字体缩放)下的不一致之处。这鼓励创建可重用、原子化的组件。
不容商榷的注意事项 这个工作流程很有效,但也很严格。只有你遵守规则时才有效。
- 无真实数据:你的预览不能获取真实数据。它不应该访问网络或数据库。
UiStateProvider提供的状态必须是静态和模拟的。 - 组合函数中无逻辑:如果你发现自己写
if (LocalInspectionMode.current)来“修复”预览错误,那么你就失败了。错误是逻辑不属于你的 UI 的症状。修复架构,而不是预览。 - UDF 是强制的:整个系统建立在单向数据流和状态提升的基础上。状态向下流,事件向上流。这不是可选的。
总结 这不是万能药。这是一个有纪律的工作流程,可以减少摩擦,并消除不写预览的最常见借口。承诺在 KMP 驱动的逻辑和 Compose 驱动的 UI 之间保持清晰的关注点分离是前提条件。
当这种分离得到尊重时,预览就不再是一件苦差事,而成为快速开发的强大工具。
如果这里的任何概念不熟悉,Jetpack Compose 官方文档是存在的。你或许应该去读一读。