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
}
四、最佳实践
- 状态“能不提升就不提升”:单个组件用的简单状态,留在内部更简洁
- 避免“属性穿透过度”:只给子组件传递它需要的状态和事件,不传递整个容器类(如 ViewModel、自定义状态类)
- 在 ***pose 中保存界面状态,界面逻辑用 rememberSaveable,业务逻辑用 ViewModel+SavedStateHandle
- 动画相关状态注意协程作用域:调用LazyListState.animateScrollToItem、DrawerState.close等挂起函数时,要用组合作用域(rememberCoroutineScope),不能直接用viewModelScope,否则会报错。
- 状态容器的稳定性:普通状态容器类要加@Stable注解,保证***pose正确优化重组。