mcpField
C
compose-state-holder-ui-split

compose-state-holder-ui-split

github.com/chrisbanes开发1.1k969 次安装1 天前更新
代码仓库

关于 compose-state-holder-ui-split

面向 Compose 开发者的状态持有者与 UI 分离技能,将状态收集和副作用留在状态持有层,让 UI 仅依赖不可变状态与回调,便于预览和测试。

主要功能

  1. 01分离状态接线与界面布局
  2. 02在状态持有层收集业务状态
  3. 03通过状态对象和回调传递 UI 契约
  4. 04在状态持有层处理副作用
  5. 05区分 UI 本地状态与业务状态

适用场景

  1. 01拆分依赖 ViewModel 的屏幕布局
  2. 02让 UI 测试无需构造完整应用依赖
  3. 03将导航事件改为显式回调

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/chrisbanes/skills --skill compose-state-holder-ui-split

安装后在 Claude Code 中输入/compose-state-holder-ui-split确认已加载。

详细介绍

Compose:状态持有者与 UI 分离

核心原则

将状态持有者的接线与 UI 渲染分离。状态持有者可组合项与 ViewModel、组件、流、导航和副作用交互。UI 可组合项接收纯粹的不可变 UI 状态和回调,并描述布局。

这样一来,屏幕更易于预览、测试,也更容易在 Android、Desktop、TV 和 KMP/CMP 目标平台之间复用。

何时使用此技能

当 Compose 屏幕符合以下情况时,使用此技能:

  • 直接接收 ViewModel、组件、控制器、导航器、存储库或服务。
  • 在布局大部分 UI 的同一个函数中收集应用/业务状态或副作用。
  • 将整个状态持有者传给子可组合项,而不是显式传递状态和回调。
  • 因为需要依赖注入、导航、生命周期或伪服务而难以预览。
  • UI 测试必须构建完整的应用栈,才能验证一个简单的布局分支。

模式

使用一个小型公开状态持有者可组合项:

@Composable
fun ProfileScreen(component: ProfileComponent, modifier: Modifier = Modifier) {
    val state by component.state.collectAsStateWithLifecycle()

    ProfileScreen(
        state = state,
        onNameChange = component::onNameChange,
        onSaveClick = component::save,
        onBackClick = component::back,
        modifier = modifier,
    )
}

然后将 UI 放入一个对状态持有者一无所知的普通可组合项中:

@Composable
fun ProfileScreen(
    state: ProfileUiState,
    onNameChange: (String) -> Unit,
    onSaveClick: () -> Unit,
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    ProfileContent(
        name = state.name,
        isSaving = state.isSaving,
        canSave = state.canSave,
        onNameChange = onNameChange,
        onSaveClick = onSaveClick,
        onBackClick = onBackClick,
        modifier = modifier,
    )
}

私有内容函数可以拆分布局:

@Composable
private fun ProfileContent(
    name: String,
    isSaving: Boolean,
    canSave: Boolean,
    onNameChange: (String) -> Unit,
    onSaveClick: () -> Unit,
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    // Layout only.
}

经验法则

关注点状态持有者可组合项UI 可组合项
收集 ViewModel/组件状态是否
收集单次触发的副作用是,或由一个精简的同级副作用处理器负责通常否
持有通过依赖注入获取的对象是否
接收不可变 UI 状态通常会透传是
接收用于用户事件的 lambda负责接线调用它们
负责布局、修饰符、语义、测试标签否/尽量少是
持有滚动、焦点、文本输入、动画、交互等 UI 局部状态有时会为其提供初始值是
适合预览/截图不一定是

“UI 可组合项中不收集状态”这条规则针对的是应用/业务状态和副作用流。普通 UI 可组合项仍可以持有 UI 局部框架状态:rememberScrollState、rememberLazyListState、FocusRequester、焦点状态、动画状态、TextFieldState、MutableInteractionSource.collectIsPressedAsState(),以及其他属于所渲染控件的类似行为。

如果这种 UI 局部状态发展成由多个相关字段和操作构成的协同行为,请使用 compose-state-hoisting 判断是否应将其改为在组合中记住的普通状态持有者类。

传递什么

传递尽可能精简且实用的 UI 契约:

  • 如果屏幕确实有状态,优先使用专门的 UiState/State 对象,而不是许多互不相关的基本类型。
  • 优先使用显式 lambda(onRetryClick、onItemSelected),而不是传递整个组件。
  • 如果领域模型会迫使业务规则进入 UI 可组合项,就不要在其中使用领域模型。若 UI 需要不同的数据结构,则映射为 UI 模型。
  • 将导航作为回调处理。UI 可组合项表达的是“用户点击了返回”,而不是“导航到路由 X”。
  • 对于变化时不应导致整棵树重新组合的帧率或 UI 局部值:优先使用提供器 lambda,并按 compose-state-deferred-reads 延迟读取。

副作用

compose-side-effects 介绍了副作用 API(LaunchedEffect、DisposableEffect、SideEffect)、键、清理和 rememberUpdatedState。

在状态持有者附近处理副作用,因为在那里可以同时访问副作用来源和命令式目标:

@Composable
fun ProfileScreen(component: ProfileComponent, snackbarHostState: SnackbarHostState) {
    val state by component.state.collectAsStateWithLifecycle()

    LaunchedEffect(component) {
        component.effects.collect { effect ->
            when (effect) {
                ProfileEffect.Saved -> snackbarHostState.showSnackbar("Saved")
            }
        }
    }

    ProfileScreen(state = state, onSaveClick = component::save)
}

如果副作用处理逻辑变得复杂,请提取 ProfileEffects(component, snackbarHostState),而不是将组件传入 UI 可组合项。

常见错误

错误问题所在修复方法
fun Screen(viewModel: MyViewModel) 包含所有布局没有 Android 生命周期和依赖注入时,难以预览和测试添加一个接收 state 和回调的纯 UI 重载
子组合函数接收 component依赖会沿组件树泄漏只传递子组件所需的状态和回调
UI 组合函数启动导航UI 会与应用路由耦合暴露 onBackClick、onItemClick 等回调
UI 组合函数收集应用或业务流程收集生命周期隐藏在布局中在状态持有者附近收集值,并向下传递
没有理由就将 UI 局部状态提升到状态持有者中状态持有者开始负责布局机制如果滚动、焦点、动画和文本字段交互状态仅属于 UI 行为,就将其保留在 UI 组合函数中
每个细小的组合函数都有一个状态持有者重载仪式感太强在屏幕或区域边界拆分,而不是为每个 Row 拆分

不适用的情况

  • 已经接收纯值和回调的微型一次性组合函数。
  • Button、Card 或 ListItem 等设计系统基础组件;它们应暴露插槽和修饰符,而不是状态持有者。
  • 状态持有者组合函数只会转发一个基础组件,无法提供隔离的情况。

相关内容

评论

支持 Markdown