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 自然就选对了!

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui