Android Compose 状态提升和状态保存

Jetpack ***pose状态提升(State Hoisting)

状态提升是***pose中管理界面状态的核心思想——把状态放到“最合适的作用域”,让状态可复用、易测试、好维护,核心原则是 “状态靠近使用处,提升至最低共同祖先实体”。

一、先搞懂3个基础概念

1. 两种逻辑

  • 业务逻辑:处理“数据规则”的逻辑,比如点击“收藏”按钮后,把新闻存到数据库、获取聊天用户建议列表。
  • 界面逻辑:处理“怎么显示”的逻辑,比如点击按钮后滚动列表到底部、切换搜索栏提示文字、导航到新页面。

2. 界面状态

就是描述界面的属性,分两种:

  • 屏幕界面状态:屏幕要显示的核心数据,比如新闻列表(NewsUiState)、聊天消息列表,通常和业务数据相关。
  • 界面元素状态:单个UI组件的固有属性,比如按钮是否隐藏、输入框文本、LazyColumn的滚动位置(LazyListState),影响组件自身呈现。

3. 最低共同祖先实体

所有需要“读/写某个状态”的可组合项,往上找最接近的共同父组件——简单说就是“找这些组件的共同上级,且越靠近使用处越好”,避免状态层级过高或过低。

二、状态提升的核心场景(按逻辑类型分)

1. 界面逻辑相关:状态在“界面内部”流转

这类状态只和UI显示、交互相关,不涉及业务数据存储(比如滚动、展开/收起)。

(1)无需提升:状态留在可组合项内部

如果状态只有单个组件用,逻辑也简单,直接放组件里就行。

  • 例子:ChatBubble组件的“展开详情”状态(showDetails),只有自己需要读/写,点击文本切换显示/隐藏时间戳,没必要提升。
  • 适用场景:动画状态、单个组件的独立交互状态。
@***posable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state

    Text(
        text = AnnotatedString(message.content),
        modifier = Modifier.clickable {
            showDetails = !showDetails // Apply UI logic
        }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}
(2)需要提升:多个可组合项共用状态

如果多个组件要操作同一个状态,就把状态提到它们的“最低共同祖先实体”。

  • 例子:聊天界面的LazyColumn(消息列表),需要两个组件操作它的滚动状态:①“跳到底部”按钮;②发送新消息后自动滚动。
  • 做法:把LazyListState从LazyColumn提升到父组件ConversationScreen,再传递给MessagesList和UserInput。
  • 好处:组件可重用(比如MessagesList能单独预览,用默认的rememberLazyListState)、逻辑统一(滚动逻辑集中在父组件)。
@***posable
private fun ConversationScreen(/*...*/) {
    val scope = rememberCoroutineScope()

    val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen

    MessagesList(messages, lazyListState) // Reuse same state in MessageList

    UserInput(
        onMessageSent = { // Apply UI logic to lazyListState
            scope.launch {
                lazyListState.scrollToItem(0)
            }
        },
    )
}

@***posable
private fun MessagesList(
    messages: List<Message>,
    lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {

    LazyColumn(
        state = lazyListState // Pass hoisted state to LazyColumn
    ) {
        items(messages, key = { message -> message.id }) { item ->
            Message(/*...*/)
        }
    }

    val scope = rememberCoroutineScope()

    JumpToBottom(onClicked = {
        scope.launch {
            lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
        }
    })
}
(3)复杂界面逻辑:用普通状态容器类

如果一个组件有多个状态字段,或逻辑复杂,就把状态和逻辑抽到“普通状态容器类”。

  • 核心作用:分离关注点——可组合项只负责画UI,容器类负责管状态和逻辑。
  • 关键技术:用remember创建容器实例,遵循可组合项生命周期;需要持久化就用rememberSaveable+自定义Saver。
// LazyListState.kt

@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
    /**
     *   The holder class for the current scroll position.
     */
    private val scrollPosition = LazyListScrollPosition(
        firstVisibleItemIndex, firstVisibleItemScrollOffset
    )

    suspend fun scrollToItem(/*...*/) { /*...*/ }

    override suspend fun scroll() { /*...*/ }

    suspend fun animateScrollToItem() { /*...*/ }
}

如何使用 ?

val scrollState = rememberLazyListState()
@***posable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex,
            initialFirstVisibleItemScrollOffset
        )
    }
}

2. 业务逻辑相关:状态放到“组合之外”(ViewModel)

这类状态和业务数据绑定(比如消息列表、用户输入建议),需要脱离UI生命周期(比如屏幕旋转后数据不丢)。

(1)ViewModel的核心作用

ViewModel是业务逻辑场景的“状态所有者”

  • 是唯一的、权威的数据真相。在整个界面(甚至多个界面)中,关于某个数据“当前是什么”,都以它为准。
  • 是​​所有需要使用该状态的组件在逻辑上最近的共同"祖先"​​
    • 我们希望这个共同“祖先”级别尽可能低,只要高到能覆盖所有需要该状态的组件即可,而不是直接提到最高级的 Activity。这样更灵活、更内聚。

viewModel负责两件事:

  • 对接业务层/数据层:比如调用MessagesRepository获取消息、生成用户输入建议。
  • 提供屏幕界面状态:把业务数据转换成UI能直接用的状态(比如用StateFlow暴露消息列表)。
(2)例子:聊天界面的ViewModel用法
  • 存储屏幕状态:messages(聊天记录)
  • 暴露业务逻辑:sendMessage(发送消息)
  • 可组合项用法:在ConversationScreen(屏幕级可组合项)中注入ViewModel,通过collectAsStateWithLifecycle收集状态
class ConversationViewModel(
    channelId: String,
    messagesRepository: MessagesRepository
) : ViewModel() {

    val messages = messagesRepository
        .getLatestMessages(channelId)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    // Business logic
    fun sendMessage(message: Message) { /* ... */ }
}

@***posable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@***posable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}
(3)注意点 : 不应传递 ViewModel

不传递ViewModel是为了避免生命周期不匹配导致的依赖泄露,同时降低组件耦合、控制状态访问范围,遵循了***pose组件复用和状态管理的设计理念。

①. 生命周期不匹配,直接引发泄露风险

ViewModel的生命周期绑定屏幕级作用域(比如Activity、导航目的地),会存活到屏幕销毁(如退出页面)。而子组件(比如LazyColumn的列表项、单个按钮)的生命周期极短——会频繁重组、销毁,甚至被复用(比如滚动时回收再创建)。

如果子组件持有ViewModel引用,会出现两种问题:一是子组件被缓存时,意外“拽着”ViewModel不让其释放,造成内存泄露;二是子组件复用在其他屏幕时,拿到不属于当前屏幕的ViewModel,导致数据错乱(比如用聊天屏幕的ViewModel给设置屏幕的组件用)。

②. 强耦合破坏组件复用性

子组件如果依赖具体的ViewModel(比如ConversationViewModel),就和该屏幕的业务逻辑绑死了。比如一个“发送按钮”组件,若接收ConversationViewModel,就只能在聊天屏幕用,无法在“发布动态”“评论”等其他需要发送功能的屏幕复用,违背了***pose组件“单一职责、可复用”的核心设计。

③. 扩大状态访问范围,增加维护成本

ViewModel是状态的“可信来源”,核心是通过统一方法控制状态修改。若传递给子组件,子组件可能直接访问或修改不该碰的状态/方法(比如子组件私自调用ViewModel的数据库删除操作),打破“状态修改统一入口”的规则。

只传递子组件必需的“具体状态+事件回调”(比如String类型的输入文本、() -> Unit类型的发送回调),能精准限制访问范围,后续修改ViewModel时,也只需调整屏幕级组件,不用改动所有子组件。

三、在***pose中保存界面状态

在 ***pose 中保存界面状态,需根据状态所属的 “界面逻辑” 或 “业务逻辑”,分别用 rememberSaveable 或 ViewModel+SavedStateHandle API,避免配置更改、系统进程终止导致状态丢失。

(1)先搞懂:为啥会丢状态?

Android 应用的界面状态可能因两种情况丢失,这是保存状态的核心前提:

  • 配置更改:比如屏幕旋转、切换深色模式,系统会销毁并重建 Activity,默认状态会消失。
  • 系统发起的进程终止:应用在后台时,系统为释放内存清理进程,进程重启后状态会重置。

注意:用户主动关闭 Activity(比如按返回键)导致的状态丢失,通常是正常的,无需处理。

(2)界面逻辑:用 rememberSaveable 保存

如果状态只和界面展示相关(比如控件是否展开、列表滚动位置),就用 rememberSaveable。

  • 存储位置:通过 Android 的保存实例状态机制,存在 Bundle 中。
  • 支持类型:默认支持基元类型(布尔、int、字符串等),非基元类型(如数据类)需用 Parcelize 注解、listSaver/mapSaver 或自定义 Saver。

控件展开 / 收起状态(比如聊天气泡):用 rememberSaveable 存储布尔值,点按后切换状态,屏幕旋转后仍保留展开 / 收起状态。

@***posable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

列表滚动位置(LazyColumn/LazyRow):用 rememberLazyListState,内部已集成 rememberSaveable,自动保留滚动位置。

@***posable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

注意事项

  • 不存大型复杂对象或对象列表,否则可能触发 TransactionTooLarge 异常。
  • 只存关键信息(如 ID、索引),复杂状态从永久性存储中恢复。

(3)业务逻辑:用 ViewModel+SavedStateHandle 保存

如果状态和业务逻辑绑定(比如用户输入、筛选条件),且需要跨界面共享,就把状态提升到 ViewModel,再用 SavedStateHandle 保存。

ViewModel 本身:能自动处理配置更改,Activity 重建后仍保留实例,状态不丢失。
SavedStateHandle:解决进程终止后状态丢失的问题,同样基于 Bundle 存储。

***pose State 类型:saveable() API
直接以 MutableState 形式读写状态,支持基元类型和自定义 Saver。
示例:存储 TextField 中的用户输入,进程重启后输入内容不丢失。

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@***posable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

数据流类型:getStateFlow() API
以只读的 StateFlow 存储状态,需通过指定键来更新和获取值。
示例:存储聊天频道的筛选条件,切换筛选后状态持久化,进程重启后恢复。

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        ***bine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

四、最佳实践

  1. 状态“能不提升就不提升”:单个组件用的简单状态,留在内部更简洁
  2. 避免“属性穿透过度”:只给子组件传递它需要的状态和事件,不传递整个容器类(如 ViewModel、自定义状态类)
  3. 在 ***pose 中保存界面状态,界面逻辑用 rememberSaveable,业务逻辑用 ViewModel+SavedStateHandle
  4. 动画相关状态注意协程作用域:调用LazyListState.animateScrollToItem、DrawerState.close等挂起函数时,要用组合作用域(rememberCoroutineScope),不能直接用viewModelScope,否则会报错。
  5. 状态容器的稳定性:普通状态容器类要加@Stable注解,保证***pose正确优化重组。
转载请说明出处内容投诉
CSS教程网 » Android Compose 状态提升和状态保存

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买