compose-state-hoisting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose state hoisting

Compose状态提升

Core principle

核心原则

Hoist state only as far as the logic needs it. Keep simple UI element state local, move shared UI element state to the lowest common composable owner, extract a plain state holder when UI-only behavior becomes a concept, and use a screen state holder when business logic or app data is involved.
仅在逻辑需要的范围内提升状态。将简单UI元素状态保留在本地,将共享UI元素状态移至最低层级的共同可组合项所有者,当纯UI行为形成独立概念时提取普通状态持有者类,涉及业务逻辑或应用数据时则使用屏幕级状态持有者。

Decision guide

决策指南

SituationOwner
One composable reads/writes simple stateKeep local with
remember
/
rememberSaveable
Sibling or parent composables need to read/write itHoist state and events to their lowest common composable ancestor
Related UI element state plus UI logic is making a composable hard to read, preview, or testExtract a plain state holder class remembered in composition
Repository calls, persistence, business rules, or screen UI state production are involvedUse a screen-level state holder such as a
ViewModel
or component
UI element state includes things like expansion, sheet visibility, scroll position, focus, text field editing state, selection, and animation/interaction state. Screen UI state is app data prepared for display.
If UI element state is an input to business logic, it may need to live in the screen state holder too. For example, text used to query repository-backed suggestions belongs with the state holder that produces those suggestions.
场景所有者
单个可组合项读写简单状态使用
remember
/
rememberSaveable
保留在本地
兄弟或父级可组合项需要读写该状态将状态和事件提升至它们的最低共同可组合项祖先
相关UI元素状态加上UI逻辑导致可组合项难以阅读、预览或测试提取一个在组合中被remember的普通状态持有者类
涉及仓库调用、持久化、业务规则或屏幕UI状态生成使用屏幕级状态持有者,例如
ViewModel
或组件
UI元素状态包括展开状态、底部栏可见性、滚动位置、焦点、文本框编辑状态、选择状态以及动画/交互状态。屏幕UI状态是为展示准备的应用数据。
如果UI元素状态是业务逻辑的输入,它可能也需要存放在屏幕级状态持有者中。例如,用于查询仓库返回建议的文本,应该与生成这些建议的状态持有者放在一起。

Plain state holder trigger

普通状态持有者提取触发条件

Extract a plain state holder when several of these are true:
  • Multiple related
    remember
    values are coordinated by the same callbacks.
  • Scroll, focus, text, selection, or sheet state needs named operations such as
    clear
    ,
    submit
    ,
    jumpToTop
    , or
    openFilters
    .
  • Derived UI flags are scattered through the composable.
  • Child composables receive mechanics they do not conceptually own.
  • Previews or tests must drive a long sequence of UI details to check one behavior.
  • Helper functions need many state parameters just to keep the composable readable.
Do not extract for one boolean, one text field, or trivial show/hide logic. Ceremony is not separation of concerns.
当满足以下多个条件时,提取普通状态持有者:
  • 多个相关的
    remember
    值由相同的回调协调。
  • 滚动、焦点、文本、选择或底部栏状态需要命名操作,例如
    clear
    submit
    jumpToTop
    openFilters
  • 派生UI标志分散在可组合项中。
  • 子可组合项接收了它们在概念上并不拥有的机制。
  • 预览或测试必须驱动一系列UI细节才能验证一个行为。
  • 辅助函数需要多个状态参数才能保持可组合项的可读性。
不要为单个布尔值、单个文本框或简单的显示/隐藏逻辑提取状态持有者。繁琐的形式不等于关注点分离。

Pattern

模式

Use a plain class for UI element state and UI logic, plus a
remember...State
function for composition-owned objects:
kotlin
@Stable
class ProductSearchState(
    query: String,
    private val listState: LazyListState,
    private val focusRequester: FocusRequester,
) {
    var query by mutableStateOf(query)
        private set

    var filtersOpen by mutableStateOf(false)
        private set

    val canClear: Boolean
        get() = query.isNotEmpty()

    fun updateQuery(value: String) {
        query = value
    }

    fun clear() {
        query = ""
        focusRequester.requestFocus()
    }

    suspend fun jumpToTop() {
        listState.animateScrollToItem(0)
    }
}

@Composable
fun rememberProductSearchState(
    initialQuery: String = "",
    listState: LazyListState = rememberLazyListState(),
    focusRequester: FocusRequester = remember { FocusRequester() },
): ProductSearchState {
    return remember(listState, focusRequester) {
        ProductSearchState(initialQuery, listState, focusRequester)
    }
}
The composable renders from the state holder and calls intent-style methods. If a parent needs to coordinate the same UI behavior, accept the state holder as a parameter with a default:
kotlin
@Composable
fun ProductSearchPanel(
    state: ProductSearchState = rememberProductSearchState(),
    modifier: Modifier = Modifier,
) {
    val scope = rememberCoroutineScope()

    SearchField(
        query = state.query,
        onQueryChange = state::updateQuery,
        onClear = state::clear,
    )

    JumpToTopButton(onClick = {
        scope.launch { state.jumpToTop() }
    })
}
使用普通类存储UI元素状态和UI逻辑,再配合
remember...State
函数处理组合所属对象:
kotlin
@Stable
class ProductSearchState(
    query: String,
    private val listState: LazyListState,
    private val focusRequester: FocusRequester,
) {
    var query by mutableStateOf(query)
        private set

    var filtersOpen by mutableStateOf(false)
        private set

    val canClear: Boolean
        get() = query.isNotEmpty()

    fun updateQuery(value: String) {
        query = value
    }

    fun clear() {
        query = ""
        focusRequester.requestFocus()
    }

    suspend fun jumpToTop() {
        listState.animateScrollToItem(0)
    }
}

@Composable
fun rememberProductSearchState(
    initialQuery: String = "",
    listState: LazyListState = rememberLazyListState(),
    focusRequester: FocusRequester = remember { FocusRequester() },
): ProductSearchState {
    return remember(listState, focusRequester) {
        ProductSearchState(initialQuery, listState, focusRequester)
    }
}
可组合项从状态持有者获取数据进行渲染,并调用意图式方法。如果父级需要协调相同的UI行为,可以将状态持有者作为参数传入并设置默认值:
kotlin
@Composable
fun ProductSearchPanel(
    state: ProductSearchState = rememberProductSearchState(),
    modifier: Modifier = Modifier,
) {
    val scope = rememberCoroutineScope()

    SearchField(
        query = state.query,
        onQueryChange = state::updateQuery,
        onClear = state::clear,
    )

    JumpToTopButton(onClick = {
        scope.launch { state.jumpToTop() }
    })
}

Composition ownership

组合所有权

Plain state holders created with
remember
follow the composable lifecycle. This makes them a good home for Compose UI objects such as
LazyListState
,
FocusRequester
,
PagerState
,
DrawerState
, and
TextFieldState
.
Keep suspend UI operations that require a frame clock, such as scroll or drawer animations, in a composition-scoped coroutine (
rememberCoroutineScope
,
LaunchedEffect
, or another composition-owned scope). Do not move those calls to
viewModelScope
.
使用
remember
创建的普通状态持有者遵循可组合项的生命周期。这使得它们非常适合存放Compose UI对象,例如
LazyListState
FocusRequester
PagerState
DrawerState
TextFieldState
需要帧时钟的挂起UI操作(如滚动或抽屉动画)应放在组合作用域的协程中(
rememberCoroutineScope
LaunchedEffect
或其他组合所属作用域)。不要将这些调用移至
viewModelScope

Saving state

状态保存

Use
rememberSaveable
or a custom
Saver
only for values that should survive Activity or process recreation, such as a query string, selected filter IDs, or a current tab key.
Do not try to save runtime objects like
LazyListState
,
FocusRequester
, coroutine scopes, or callbacks directly. Save the minimal serializable values needed to rebuild behavior.
仅对需要在Activity或进程重建后保留的值使用
rememberSaveable
或自定义
Saver
,例如查询字符串、选中的过滤器ID或当前标签键。
不要尝试直接保存运行时对象,如
LazyListState
FocusRequester
、协程作用域或回调。只需保存重建行为所需的最小可序列化值即可。

Common mistakes

常见错误

MistakeFix
Hoisting every local state value to a parent "just in case"Hoist to the lowest owner that actually reads or writes it
Extracting a plain state holder for one booleanKeep simple private UI state local
Putting repository calls or product rules in a Compose state holderMove that logic to a screen state holder such as a
ViewModel
or component
Keeping text or selection local when it drives repository-backed screen stateMove that input to the screen state holder with the business logic
Passing a state holder deep into unrelated childrenPass plain values and callbacks unless the child truly coordinates the holder's behavior
Treating the holder as a dumping ground for a whole screenSplit by cohesive UI behavior, such as search input, sheet coordination, or list controls
Calling animation suspend functions from
viewModelScope
Use a composition-scoped coroutine
错误修复方案
为了“以防万一”将所有本地状态提升至父级仅将状态提升至实际需要读写它的最低层级所有者
为单个布尔值提取普通状态持有者将简单的私有UI状态保留在本地
将仓库调用或产品规则放在Compose状态持有者中将该逻辑移至屏幕级状态持有者,例如
ViewModel
或组件
当文本或选择状态驱动仓库返回的屏幕状态时,仍将其保留在本地将该输入移至包含业务逻辑的屏幕级状态持有者中
将状态持有者深层传递给无关的子项除非子项真正需要协调持有者的行为,否则传递普通值和回调即可
将持有者当作整个屏幕的“垃圾场”按内聚的UI行为拆分,例如搜索输入、底部栏协调或列表控制
viewModelScope
调用动画挂起函数
使用组合作用域的协程

Related

相关内容

  • compose-state-authoring
    — correct local
    remember
    and mutable state authoring.
  • compose-state-holder-ui-split
    — split screen state-holder wiring from plain state-driven UI rendering.
  • compose-side-effects
    — choose effect APIs and composition-scoped coroutine boundaries.
  • compose-focus-navigation
    — focus state, requesters, and keyboard/D-pad behavior.
  • compose-state-authoring
    — 正确的本地
    remember
    和可变状态编写方式。
  • compose-state-holder-ui-split
    — 将屏幕状态持有者的连接逻辑与纯状态驱动的UI渲染分离。
  • compose-side-effects
    — 选择副作用API和组合作用域的协程边界。
  • compose-focus-navigation
    — 焦点状态、请求器以及键盘/方向键行为。