Jetpack Compose 核心:状态管理 + Lazy 列表
一、什么是 Compose 中的状态 (State)
通俗一句话:状态 = 描述 UI 在某一时刻的数据。界面上一切会变的东西,背后都是状态。
现实例子
- 输入框里输入的文字
- 计数器的数字
- 开关是否打开
- 列表的数据
- 加载中 / 成功 / 错误
- 弹窗是否显示
这些会随时间、用户操作发生变化的数据 ,就是状态。
UI 就是状态的可视化表现:UI = 函数 (状态),同一个状态,渲染出来的界面就固定。
普通 Android View 体系:先改 View 对象
textView.text = "xxx",界面才变。Compose 声明式:修改数据 (状态),框架自动刷新 UI,你不去直接操作控件。
普通变量为什么不是「Compose 可感知的状态」
kotlin
// 普通局部变量,只是一块内存数据
var count = 0
它是数据,但不是 Compose 的 State。
- 值可以改变;
- 但是 Compose 没有订阅这个变量;
- 变量变了,Compose 不知道,不会自动跑重组刷新界面。
普通变量:数据存在,但没有通知机制。
Compose
MutableState<T>:数据 + 订阅通知机制。
mutableStateOf(0) 返回的对象做两件事:
- 存数据(value)
- 一套订阅回调:
- 在 Composable 里读取
.value→ 当前 Composable 订阅这个 State - 修改
.value→ 通知所有订阅者,触发重组,重新执行 UI 函数
👉 Compose 里说的 "状态",特指带有订阅通知能力的可观察持有者:MutableState / StateFlow 这类。
区分两个概念:数据 vs 状态包装器
- 原始数据 :
Int、String、List,纯数据,无通知能力。 - 状态包装器 :
MutableState<T>/StateFlow<T>,把原始数据包起来,增加订阅通知。
kotlin
val rawData: Int = 0 // 原始数据,不是状态
val stateWrapper: MutableState<Int> = mutableStateOf(0) // 状态包装器
remember {}本身不是状态 !remember 只是缓存对象,它缓存的里面放
mutableStateOf(),才有状态能力。
kotlin
remember { 0 } // ❌只是记住数字0,没有通知,不是状态
remember { mutableStateOf(0) } // ✅缓存 MutableState,这才是状态
rememberSaveable同理:负责跨重建保存这个状态对象,本身不产生状态。
二、 状态的分类(按存放位置)
1. 本地 UI 状态(组件内状态)
存在 Composable 内部:remember { mutableStateOf() } 生命周期:跟随组件,组件消失,状态销毁。
例子:展开 / 折叠、开关状态。
2. 保留本地状态
rememberSaveable { mutableStateOf() } 同样组件内,但是屏幕旋转 Activity 重建,状态数据保留。
例子:输入框文本、勾选框。
3. 业务状态(ViewModel)
存在 ViewModel,用StateFlow包装。
生命周期跟随 Activity/Fragment,独立于 Composable。
例子:网络请求返回列表、登录用户信息。
4. 提升后的状态
状态放在父组件,子组件只接收参数,不持有状态(状态提升)。
子组件拿到的是
value:String,这是状态的值,不是状态对象本身;修改通过回调通知父。
kotlin
@Composable
fun Parent(){
// 这里拥有状态对象
var text by remember { mutableStateOf("") }
// 传给子组件的只是状态的当前值,子组件不能直接修改state对象
Child(value = text, onValueChange = {text=it})
}
简单点记忆:
- 状态 = 会变的数据 + 通知 UI 更新的能力
- 普通变量:会变,但不会通知 Compose → UI 不刷新
- MutableState:会变,并且通知订阅的 Composable → UI 自动刷新
- remember:负责缓存状态对象;rememberSaveable:旋转重建也保存状态对象
- ViewModel+StateFlow:把状态放到 Composable 之外,管理业务数据
三、State 可观察状态原理
1. MutableState
mutableStateOf<T>() 返回 MutableState<T>
kotlin
interface MutableState<T> : State<T> {
override var value: T
}
- 读取
state.value:建立 Composable 与 State 的订阅依赖 - 修改
state.value:通知所有订阅的 Composable,触发重组
❌错误示例(无 UI 刷新)
kotlin
@Composable
fun BadDemo(){
var count = 0
Column {
Text("$count")
Button({count++}){Text("+1")}
}
}
count 是普通局部变量,点击后内存值变了,但没有任何订阅,Compose 完全不知道,UI 永远不变。
✅正确写法 1:直接使用 MutableState
kotlin
val countState = mutableStateOf(0)
Text("${countState.value}")
Button(onClick = { countState.value++ }){ Text("+1") }
✅正确写法 2:属性委托 by,省略.value,项目最常用
kotlin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
var count by mutableStateOf(0)
Text("$count")
Button({ count++ }){ Text("+1") }
by是 Kotlin 属性委托,编译器帮我们自动生成.value的 get/set。
2. State 读取位置注意点
只有在 @Composable 函数直接或者间接执行过程中读取 state.value,才会建立订阅。
- ✅在 Composable 函数体、Text 参数、Modifier 内读取 → 订阅,状态变化会重组
- ❌在
Button onClick、协程、普通回调里面读取:不会建立订阅,只是读取当前值,不会监听变化。
kotlin
// onClick只是读取,不产生订阅,onClick内部修改才会触发别处重组
Button(onClick = { count++ }) { ... }
四、remember /rememberSaveable 深度解析
1 remember
fun <T> remember(calculation: () -> T): T
作用:在重组(Recompose)过程中缓存对象实例,避免每次重组重复创建对象 ⚠️边界:Activity 重建(屏幕旋转、系统内存重启、页面销毁重建),remember 缓存全部清空,重新执行 lambda
执行时机:
- 第一次组合:执行 lambda,保存返回对象
- 后续重组:直接返回缓存对象,不再执行 lambda
- Activity 销毁重建:缓存销毁,重新执行 lambda
kotlin
@Composable
fun RememberDeepDemo() {
// 仅第一次组合执行 mutableStateOf(0)
var count by remember { mutableStateOf(0) }
Column {
Text("count=$count")
Button({ count++ }) { Text("Add") }
}
}
❗高频踩坑:不要把
remember { var count = 0 },普通变量包进 remember 没用!remember 保存的是 lambda 返回的对象,必须内部返回
mutableStateOf()可观察对象。
remember 的 key 重载(很少用)
kotlin
remember(key1, key2){ ... }
当 key 发生变化时,缓存失效,重新执行 lambda;适合根据外部参数重置状态。
kotlin
@Composable
fun InputDemo(userId: Int){
// userId变化,重置输入框文字
var text by remember(userId){ mutableStateOf("") }
TextField(value = text, onValueChange = {text=it})
}
2 rememberSaveable
remember的升级版,基于 AndroidBundle保存状态,屏幕旋转、系统内存回收重建 Activity,状态不会丢失
kotlin
var count by rememberSaveable { mutableStateOf(0) }
表格
| API | Compose 重组 | Activity 重建 (旋转) | 底层存储 |
|---|---|---|---|
| 普通 var | ❌丢失 | ❌丢失 | 内存栈 |
| remember | ✅保留 | ❌丢失 | Compose 内部缓存 |
| rememberSaveable | ✅保留 | ✅保留 | Bundle |
rememberSaveable 限制
Bundle 支持的数据类型有限:
✅支持:Int、String、Boolean、Parcelable
❌不支持普通自定义类;存自定义对象需要自定义 Saver
自定义 Saver 示例保存自定义对象
kotlin
data class User(val id:Int, val name:String)
val UserSaver = Saver<User, String>(
save = { it.name },
restore = { User(1, it) }
)
@Composable
fun SaverDemo(){
var user by rememberSaveable(saver = UserSaver) {
mutableStateOf(User(1,"test"))
}
}
使用场景区分
- UI 临时状态,旋转不需要保留 :
remember { mutableStateOf() },比如展开收起动画状态 - 输入框文字、勾选状态,旋转需要保留 :
rememberSaveable - 业务数据、网络数据:不要用 rememberSaveable,交给 ViewModel,Bundle 容量有限,不适合存大量业务数据。
五、状态提升 State Hoisting(受控组件模式)
概念
将组件内部状态提取到父组件;子组件不持有任何内部状态 ,只接收输入参数 + 回调 lambda。
单向数据流:父向下传递状态;子向上回调事件。
非受控组件(不推荐)
子组件内部自己维护状态,外部无法控制,复用困难,不好测试。
kotlin
@Composable
fun BadInput(){
// 状态写死在组件内部,外部拿不到文本
var text by remember { mutableStateOf("") }
TextField(text,onValueChange={text=it})
}
✅状态提升后受控组件(标准 Compose 写法)
kotlin
// 父组件:持有状态
@Composable
fun ParentScreen(){
var inputText by remember { mutableStateOf("") }
// 向下传状态,向上传修改回调
MyInput(
value = inputText,
onValueChange = { newText -> inputText = newText }
)
}
// 纯展示组件,无内部状态,完全受控
@Composable
fun MyInput(
value: String,
onValueChange: (String) -> Unit
){
TextField(
value = value,
onValueChange = onValueChange
)
}
什么时候必须做状态提升
- 状态需要被多个组件共享
- 需要父组件控制子组件状态
- 组件需要复用、单元测试
- 需要把状态交给 ViewModel 管理
Compose 官方最佳实践:尽可能写受控组件,减少组件内部私有状态。
六、ViewModel + Flow 完整深度讲解
remember /rememberSaveable 只是 UI 层局部状态,生命周期依附 Composable。
ViewModel 生命周期:依附 Activity/Fragment,屏幕旋转不会销毁;页面真正 finish 才销毁 。
业务逻辑、网络请求、数据库操作、业务状态,统一放在 ViewModel。
依赖
kotlin
dependencies {
// compose viewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2"
// lifecycle runtime ktx,flow生命周期感知
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.2"
}
1 StateFlow vs SharedFlow
StateFlow:保存最新状态值,有初始值,适合 UI 状态;UI 只需要最新一份界面状态。SharedFlow:事件流,无状态,适合一次性事件(弹窗、Toast、导航跳转)。
标准 ViewModel 模板(UI 状态单一数据源)
class ArticleViewModel : ViewModel() {
// 私有可变,内部修改
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
// 对外暴露只读StateFlow,UI层只能读不能修改
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
sealed class UiState {
object Loading : UiState()
data class Success(val list: List<String>) : UiState()
data class Error(val msg: String) : UiState()
}
fun loadData(){
viewModelScope.launch {
_uiState.value = UiState.Loading
delay(1000)
_uiState.value = UiState.Success(listOf("A","B","C"))
}
}
}
viewModelScope:ViewModel 自带协程作用域,ViewModel 销毁自动 cancel 协程,不会内存泄漏。
2 Compose 收集 Flow:collectAsStateWithLifecycle()
❌禁止直接使用
collectAsState()collectAsState():不会感知 Android 生命周期,后台 Activity 不可见时仍然订阅 Flow,持续接收数据,造成不必要重组、内存消耗。
✅collectAsStateWithLifecycle() 生命周期安全
- 前台 RESUMED:订阅 Flow,接收数据更新 UI
- 后台 STOPPED:自动取消订阅,暂停接收数据
- 回到前台自动恢复订阅
kotlin
@Composable
fun ArticleScreen(vm: ArticleViewModel = viewModel()) {
// 收集StateFlow转为Compose State
val uiState by vm.uiState.collectAsStateWithLifecycle()
when(val state = uiState){
is ArticleViewModel.UiState.Loading -> Text("加载中")
is ArticleViewModel.UiState.Success -> {
LazyColumn {
items(state.list){ Text(it) }
}
}
is ArticleViewModel.UiState.Error -> Text(state.msg)
}
}
3 一次性事件:Toast / 弹窗 / 导航(SharedFlow)
StateFlow 会保存值,页面重建会重复消费事件(重复弹 Toast),一次性事件用 SharedFlow
kotlin
class EventViewModel : ViewModel() {
private val _toastEvent = MutableSharedFlow<String>()
val toastEvent = _toastEvent
fun triggerToast(){
viewModelScope.launch {
_toastEvent.emit("操作成功")
}
}
}
Compose 中使用 LaunchedEffect 消费 SharedFlow 事件。
4 分层状态选型对照表表格
| 状态类型 | 适用场景 | 生命周期 |
|---|---|---|
remember { mutableStateOf() } |
UI 局部临时状态:开关、展开收起 | Composable,旋转丢失 |
rememberSaveable |
输入框、勾选,旋转需要保留 | Activity 重建保留,Bundle 存储 |
ViewModel + StateFlow |
业务数据、网络、数据库,多组件共享 | Activity 生命周期,旋转存活 |
七、Lazy 系列组件深度解析(LazyColumn / LazyRow)
Column:一次性渲染全部子组件,数据量大直接卡顿,等价 LinearLayoutLazyColumn:Compose 的 RecyclerView,只渲染屏幕可见区域 item,滚动复用
内部原理:按需组合,不在屏幕的 item 会被丢弃,释放内存。
.1 DSL 基础
kotlin
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState, // 滚动状态控制
contentPadding = PaddingValues(16.dp), // 整体内边距
verticalArrangement = Arrangement.spacedBy(8.dp) // item间距
) {
// 单个item,只生成一项
item { Text("头部标题") }
// 批量列表
items(dataList, key = { item -> item.id }) { item ->
ListItemCard(item)
}
// 尾部
item { Text("底部") }
}
.2 key 机制【重中之重,性能必看】
如果不指定
key,Lazy 使用数组下标作为 key。当列表增删、排序之后,下标改变,会出现item 数据错乱、闪烁、错位。
✅正确:使用业务唯一 id 作为 key
kotlin
items(
items = dataList,
key = { model -> model.id } // 使用业务唯一主键
) { model ->
ItemCard(model)
}
key 作用:Lazy 靠 key 识别 item,数据变更时可以复用已存在的 Composable 实例,最小化重组,消除闪烁。
.3 LazyListState 滚动控制
kotlin
@Composable
fun LazyScrollDemo(){
val listState = rememberLazyListState()
LazyColumn(state = listState){
items(100){ Text("$it") }
}
// 滚动到指定索引
LaunchedEffect(Unit) {
listState.animateScrollToItem(index = 30)
}
// 获取当前滚动位置
val firstVisible = listState.firstVisibleItemIndex
}
常用场景:下拉刷新、上拉加载更多、记录滚动位置。
上拉加载更多:监听
listState,判断是否滚动到底部,触发加载下一页。
.4 Lazy 组件常见坑
- ❌LazyColumn 内部 item 不要使用
Modifier.weight()weight 需要父组件确定高度,Lazy 子项高度未确定,会报错或者布局异常。 - ❌不要假设所有 item 都存在,屏幕外 item 不会被组合,拿不到子组件尺寸。
- ❌不要在 Lazy 的 item 内部做重量级计算,滚动会频繁重组。
- ✅列表数据变化务必提供 key;
- ✅
LazyVerticalGrid网格,LazyRow横向列表,DSL 语法完全一致。
.5 LazyColumn + ViewModel 完整示例
kotlin
@Composable
fun LazyListVmScreen(vm: ArticleViewModel = viewModel()){
val uiState by vm.uiState.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
when(val s = uiState){
is ArticleViewModel.UiState.Success -> {
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()){
items(s.list, key={it}){
Text(it, Modifier.padding(12.dp))
}
}
}
ArticleViewModel.UiState.Loading -> Text("loading")
is ArticleViewModel.UiState.Error -> Text(s.msg)
}
}
八、整体流程图
业务数据(网络/DB)
↓
ViewModel(StateFlow)
↓ collectAsStateWithLifecycle()
Compose State
↓
UI = f(State)
├─ 普通控件 Text/Button/TextField
└─ LazyColumn 列表渲染
↑
用户交互点击输入 → 回调 → ViewModel修改状态
完整Demo:
gradle
dependencies {
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.activity:activity-compose:1.10.0")
implementation(platform("androidx.compose:compose-bom:2026.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
// viewModel compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.2")
debugImplementation("androidx.compose.ui:ui-tooling")
}
MainActivity.kt 完整代码
kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
// ---------------------- 数据模型 ----------------------
data class ItemData(
val id: Int,
val title: String,
var isSelect: Boolean // 选中状态
)
// ---------------------- UI密封状态 ----------------------
sealed class ListUiState {
object Loading : ListUiState()
data class Success(val dataList: List<ItemData>) : ListUiState()
data class Error(val message: String) : ListUiState()
}
// ---------------------- ViewModel ----------------------
class ListDemoViewModel : ViewModel() {
private val _uiState = MutableStateFlow<ListUiState>(ListUiState.Loading)
val uiState: StateFlow<ListUiState> = _uiState.asStateFlow()
// 模拟网络请求加载列表
fun fetchList() {
viewModelScope.launch {
_uiState.value = ListUiState.Loading
delay(1500) // 模拟网络耗时
val mockData = (1..30).map {
ItemData(id = it, title = "列表条目 $it", isSelect = false)
}
_uiState.value = ListUiState.Success(mockData)
}
}
// 修改item选中状态
fun toggleItemSelect(targetId: Int) {
val currentState = _uiState.value
if (currentState is ListUiState.Success) {
val newList = currentState.dataList.map { item ->
if (item.id == targetId) {
item.copy(isSelect = !item.isSelect)
} else item
}
_uiState.value = ListUiState.Success(newList)
}
}
}
// ---------------------- Activity入口 ----------------------
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
ListDemoScreen()
}
}
}
}
}
// ---------------------- Compose页面 ----------------------
@Composable
fun ListDemoScreen(
vm: ListDemoViewModel = viewModel()
) {
// remember本地UI状态:页面内临时计数,仅重组保留,旋转会丢失
var clickTotalCount by remember { mutableStateOf(0) }
// 生命周期安全收集StateFlow
val uiState by vm.uiState.collectAsStateWithLifecycle()
// 首次进来加载数据
androidx.compose.runtime.LaunchedEffect(Unit) {
vm.fetchList()
}
Column(modifier = Modifier.fillMaxSize()) {
// 头部:本地状态展示,统计点击总次数
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "条目点击总次数:$clickTotalCount", style = MaterialTheme.typography.titleMedium)
}
// 根据UiState渲染不同页面
when (val state = uiState) {
is ListUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("正在加载数据...")
}
}
is ListUiState.Error -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = state.message, color = MaterialTheme.colorScheme.error)
}
}
is ListUiState.Success -> {
val dataList = state.dataList
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 重点:key 使用业务id,避免列表滑动闪烁错乱
items(
items = dataList,
key = { it.id }
) { item ->
// 状态提升:把数据和点击回调传给子组件
ListItemCard(
item = item,
onItemClick = {
// 本地状态修改
clickTotalCount++
// 通知ViewModel修改业务状态
vm.toggleItemSelect(item.id)
}
)
}
}
}
}
}
}
// ---------------------- 受控子组件:ListItemCard(无内部状态) ----------------------
@Composable
fun ListItemCard(
item: ItemData,
onItemClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onItemClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge
)
Text(
text = if (item.isSelect) "✅已选中" else "⬜未选中",
modifier = Modifier.padding(top = 4.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
plaintext
用户点击卡片
↓
onItemClick回调
├─ clickTotalCount++ remember本地UI状态更新 → 头部文字重组刷新
└─ vm.toggleItemSelect(id) → ViewModel更新StateFlow业务数据
↓
collectAsStateWithLifecycle 捕获Flow新值
↓
LazyColumn收到新列表数据,仅变化Item执行重组刷新UI