Jetpack Compose 的状态使用之“界面状态”

🌰 场景:点奶茶理解界面状态

打开 App 点奶茶时,你会看到:

  • 商品图片
  • "珍珠奶茶 ¥15"
  • 【+】按钮(用于增加数量)
  • 灰色的【加入购物车】按钮

当你点了两下【+】,数量变成 2,按钮突然变亮了!

👉 核心问题:App 是怎么知道"该把按钮变亮"的?

答案:它有一个"小本本"记录当前状态------这个"小本本",在 Compose 里就是界面状态(UI State)

📝 什么是"界面状态"?

界面状态 = 决定 UI 呈现样式的所有信息,例如:

  • 商品名称、价格
  • 当前选中的数量
  • 按钮是否可点击
  • 是否正在加载(转圈圈)
  • 报错信息(如"库存不足!")

只要这些信息发生变化,界面就需要同步更新。

🔄 传统做法 vs Compose 做法

❌ 以前(XML 时代)

需要手动操作每一步:找按钮 → 调用 setEnabled(true) → 手动修改颜色 → 手动更新文字......

类比:像服务员端着托盘,你喊一句,他跑一趟。

✅ 现在(Compose 时代)

只需定义规则:"如果数量 > 0,按钮就亮;否则灰着。"

只要修改"数量",Compose 会自动重绘整个界面!

类比:告诉智能厨房"订单数量大于 0,就启动打包流程",改数字后全自动响应,无需指挥每一步。

🛠 界面状态的"管家":ViewModel

想象 App 有个专职管家(ViewModel),核心职责:

  1. 记住所有重要的界面状态
  2. 处理点单、查库存、调用网络等逻辑
  3. 把最新状态同步给界面

代码示例:ViewModel 托管状态

kotlin 复制代码
// 管家的"小本本":定义状态结构
data class OrderUiState(
    val productName: String = "珍珠奶茶",
    val price: Int = 15,
    val quantity: Int = 0,
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

// 管家本身:管理状态和业务逻辑
class OrderViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(OrderUiState())
    val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()

    fun addToCart() {
        if (_uiState.value.quantity > 0) {
            // 模拟加购逻辑
            println("已加入 ${_uiState.value.quantity} 杯!")
        }
    }

    fun increaseQuantity() {
        _uiState.update { it.copy(quantity = it.quantity + 1) }
    }
}

Compose 界面:仅负责 "按状态渲染"

kotlin 复制代码
@Composable
fun OrderScreen(viewModel: OrderViewModel) {
    // 👀 看一眼管家的小本本
    val uiState by viewModel.uiState.collectAsState()
    Box(
        modifier = Modifier.padding(16.dp),
        contentAlignment = Alignment.TopStart
    ) {
        Column(
            modifier = Modifier
                .width(200.dp)
                .padding(end = 80.dp, bottom = 80.dp)
        ) {
            Text("${uiState.productName} ${uiState.price}", fontSize = 18.sp)
            Text("数量: ${uiState.quantity}", fontSize = 16.sp,
                modifier = Modifier.padding(vertical = 8.dp))

            Button(
                onClick = { viewModel.addToCart() }, enabled = uiState > 0
            ) {
                Text("加入购物车", fontSize = 14.sp)
            }
        }

        FloatingActionButton(
            onClick = { viewModel.increaseQuantity() },
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .offset(x = (-16).dp, y = (-16).dp)
        ) {
            Icon(Icons.Default.Add, contentDescription = "加一杯")
        }
    }
}
✨ 神奇之处:

你只要调 viewModel.increaseQuantity(),

uiState.quantity 变了 → 按钮自动变亮 → 数字自动更新!

你不用写一行"更新 UI"的代码。

💡 为什么要把状态交给 ViewModel?

  • 屏幕旋转不怕丢
    (Activity 重建,ViewModel 还在)
  • 逻辑集中,好维护
    (所有"加数量""查库存"都在管家那)
  • 多个界面能共享
    (购物车页也能看到数量)
  • 测试方便
    (不用启动 App,直接测 ViewModel)

回顾:什么是"界面状态"?

还是那个点奶茶的 App:

  • 商品名、价格
  • 买了几杯
  • 按钮能不能点
  • 正在加载吗?出错了吗?

这些决定界面长什么样的信息,就是界面状态(UI State)

而 Compose 的魔法是:你只管改状态,它自动更新界面!

但问题来了:这些状态,到底用什么"工具"来存和改?

下面我们就来看看------Compose 里管理界面状态的"工具箱"有哪些?

🧰 工具箱一:StateFlow + ViewModel ------ 管家的"正式小本本"

✅ 适用场景:业务数据、跨组件共享、需要持久化的状态(比如用户信息、列表数据、加载状态)

🔧 核心 API:

  • MutableStateFlow() → 可修改的小本本
  • StateFlow → 只读版(给 UI 看)
  • viewModelScope.launch → 在管家后台做事(比如调网络)
  • collectAsState() → UI 订阅管家的小本本

🛠 示例:奶茶订单状态

kotlin 复制代码
sealed class SnackbarState {
    // 无提示(默认状态)
    object None : SnackbarState()
    // 普通提示
    data class ShowMessage(val message: String) : SnackbarState()
    // 带操作按钮的提示
    data class ShowMessageWithAction(
        val message: String,
        val actionLabel: String,
        val onAction: () -> Unit, // 操作按钮的回调(UI 层执行)
    ) : SnackbarState()
}

// 1. 定义"小本本"的格式
data class OrderUiState(
    val productName: String = "珍珠奶茶",
    val quantity: Int = 0,
    val isLoading: Boolean = false,
    val error: String? = null,
)

// 2. 管家(ViewModel)保管小本本
class OrderViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(OrderUiState())
    val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()

    private val _snackbarState = MutableStateFlow<SnackbarState>(SnackbarState.None)
    val snackbarState: StateFlow<SnackbarState> = _snackbarState.asStateFlow()

    fun addOneCup() {
        _uiState.update { it.copy(quantity = it.quantity + 1) }
    }

    fun placeOrder() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                // 模拟下单
                delay(1000)
                _snackbarState.update { SnackbarState.ShowMessage("下单成功!") }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = "下单失败") }
            } finally {
                _uiState.update { it.copy(isLoading = false) }
            }
        }
    }

    // 4. 重置提示状态(UI 层操作后调用)
    fun resetSnackbarState() {
        _snackbarState.update { SnackbarState.None }
    }
}

// 3. UI 订阅小本本
@Composable
fun OrderScreen(viewModel: OrderViewModel) {
    val uiState by viewModel.uiState.collectAsState() // 👈 订阅!
    // 1. Snackbar 核心状态(UI 层持有)
    val snackbarHostState = remember { SnackbarHostState() }
    val snackbarState = viewModel.snackbarState.collectAsStateWithLifecycle()

    LaunchedEffect(snackbarState.value) {
        when (val state = snackbarState.value) {
            is SnackbarState.ShowMessage -> {
                snackbarHostState.showSnackbar(state.message)
                viewModel.resetSnackbarState()
            }

            is SnackbarState.ShowMessageWithAction -> {}

            SnackbarState.None -> {}
        }
    }
    if (uiState.isLoading) {
        CircularProgressIndicator()
    } else if (uiState.error != null) {
        Text(uiState.error!!, color = Color.Red)
    } else {
        Button(onClick = { viewModel.addOneCup() }) {
            Icon(Icons.Default.Add, "添加")
        }
        Button(
            onClick = { viewModel.placeOrder() },
            enabled = uiState.quantity > 0
        ) {
            Text("下单 (${uiState.quantity} 杯)")
        }
    }
    SnackbarHost(hostState = snackbarHostState)
}

注:如果viewModel.launch出现红色提示,是缺少Compose协程相关依赖,请依赖这个库:

Groovy 复制代码
androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" }

💡 为什么用它?

屏幕旋转不怕丢

多个界面能共用

支持复杂逻辑(网络、数据库)

Google 官方推荐!

🧰 工具箱二:mutableStateOf + remember ------ 临时便签纸

✅ 适用场景:纯 UI 交互、不涉及业务逻辑的临时状态(比如搜索框输入、开关是否打开)

🔧 核心 API:

  • mutableStateOf(initialValue) → 创建可变状态
  • remember { ... } → 重组时不丢内容
  • by 解构 → 写起来像普通变量

🛠 示例:搜索框

kotlin 复制代码
@Composable
fun SearchBar() {
    // 一张"便签纸",记着当前输入
    var query by remember { mutableStateOf("") }
    TextField(
        value = query,
        onValueChange = { query = it }, // 一改,自动刷新
        label = { Text("搜奶茶...") }
    )
    // 实时显示结果
    Text("你正在搜:$query")
}

⚠️ 注意:这张"便签纸"只在当前屏幕有效。

如果你退出再回来,内容就没了(除非用 rememberSaveable)。

🧰 工具箱三:rememberSaveable ------ 防丢便签纸

✅ 适用场景:需要在屏幕旋转、进程重建后保留的状态(比如表单填写一半)

kotlin 复制代码
var comment by rememberSaveable { mutableStateOf("") }

OutlinedTextField(
    value = comment,
    onValueChange = { comment = it },
    label = { Text("留言") }
)
  • 和 remember 几乎一样,但更持久
  • 自动保存到 Bundle(类似 Activity 的 onSaveInstanceState)

🧰 工具箱四:derivedStateOf ------ 智能便签(只在需要时更新)

✅ 适用场景:从其他状态"算出来"的值,避免无效刷新

比如:"是否显示'回到顶部'按钮?"

kotlin 复制代码
val listState = rememberLazyListState()

// 智能判断:只有滚动位置变了,才重新计算
val showScrollToTop by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 5
    }
}

if (showScrollToTop) {
    FloatingActionButton(onClick = { /* 滚动 */ }) {
        Icon(Icons.Default.ArrowUpward, "Top")
    }
}

💡 好处:即使 listState 频繁变化,只要结果没变(比如一直在第 3 行),就不会触发重组,性能更好!

🆚 一张表看懂怎么选

✅ 最佳实践口诀

  • 业务状态找管家(ViewModel + StateFlow)
  • UI 交互用便签(remember + mutableStateOf)
  • 怕丢就加 Saveable
  • 算出来的用 derived,性能更稳不白刷

🎯 总结

Compose 的界面状态不是玄学,而是一套清晰的分工体系:

重要的、复杂的、要共享的状态 → 交给 ViewModel,用 StateFlow

临时的、局部的、纯 UI 的状态 → 用 remember + mutableStateOf

你只需要问自己:

"这个信息,是整个业务需要记住的,还是只是这个输入框自己用的?"

答案一出,API 自然就选对了!

相关推荐
_李小白10 小时前
【Android FrameWork】第二十六天:BroadcastReceiver
android
@#---11 小时前
如何准确判断json文件并且拿到我想要的信息
android·python·json
喜熊的Btm12 小时前
探索 Kotlin 的不可变集合库
kotlin·android jetpack
程序员陆业聪13 小时前
Android插件化原理与方案详解
android
惟恋惜14 小时前
Jetpack Compose 界面元素状态(UI Element State)详解
android·ui·android jetpack
_李小白15 小时前
【Android FrameWork】延伸阅读:IGraphicBufferProducer驱动UI绘制过程
android·ui
_李小白16 小时前
【Android FrameWork】第二十八天:Activity 的 UI 绘制全过程
android·ui
_李小白17 小时前
【Android FrameWork】第三十天:Surface创建流程解析
android
元亓亓亓17 小时前
考研408--操作系统--day8--操作系统--虚拟内存&请求分页&页面置换/分配
android·java·开发语言·虚拟内存