第七篇:状态提升与单向数据流——架构设计的核心

7.1 什么是状态提升(State Hoisting)

状态提升:将 Composable 内部的状态移到它的调用方中管理。

听起来简单,但这是 Compose 架构设计中最重要、最基础的模式。

为什么需要状态提升?

kotlin 复制代码
// ❌ 没有状态提升 ------ 状态在组件内部,外部无法控制
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }
    
    TextField(
        value = text,
        onValueChange = { text = it }
    )
}

// 使用方无法:
// 1. 读取输入内容
// 2. 设置默认值
// 3. 监听输入变化
kotlin 复制代码
// ✅ 状态提升 ------ 状态由调用方管理
@Composable
fun MyTextField(
    text: String,            // 状态从外部传入
    onTextChange: (String) -> Unit  // 事件向上通知
) {
    TextField(
        value = text,
        onValueChange = onTextChange
    )
}

// 使用方可以:
@Composable
fun SignUpForm() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    
    Column {
        MyTextField(
            text = email,
            onTextChange = { email = it }
        )
        MyTextField(
            text = password,
            onTextChange = { password = it }
        )
        Button(
            onClick = { submit(email, password) },
            enabled = email.isNotBlank() && password.isNotBlank()
        ) {
            Text("Submit")
        }
    }
}

7.2 状态提升的标准模式

Compose 组件遵循一个被广泛认可的命名约定:

kotlin 复制代码
// 标准状态提升接口
@Composable
fun ExpandableCard(
    expanded: Boolean,              // 状态(当前是什么)
    onExpandedChange: (Boolean) -> Unit,  // 事件(变化通知)
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Surface(
        modifier = modifier.clickable { onExpandedChange(!expanded) }
    ) {
        Column {
            // 根据状态决定显示方式
            if (expanded) {
                content()
            }
            // 展开/收起指示器
            Icon(
                if (expanded) Icons.Default.ExpandLess
                else Icons.Default.ExpandMore,
                contentDescription = "Toggle"
            )
        }
    }
}

状态提升的三条规则

规则 说明
状态上移 状态至少提升到所有需要读取它的组件最近的共同父级
状态不可变暴露 对外暴露 val 状态,而非 MutableState
事件回调 状态变化通过回调通知父级,遵循单向数据流

7.3 单向数据流(UDF)

Unidirectional Data Flow 是 Compose 架构设计的基础模式:

核心原则

  1. 数据单向流动:状态从父级流向子级(通过参数)
  2. 事件单向流动:事件从子级向上通知父级(通过回调)
  3. 单一可信源:状态只在一个地方定义和修改
kotlin 复制代码
// ViewModel ------ 状态的源头
class SearchViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(SearchUiState())
    val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()
    
    fun onQueryChanged(query: String) {
        _uiState.update { it.copy(query = query) }
    }
    
    fun onSearch() {
        // 发起搜索请求...
    }
}

data class SearchUiState(
    val query: String = "",
    val results: List<Result> = emptyList(),
    val isLoading: Boolean = false
)

// Composable ------ 只消费状态和发送事件
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    Column {
        SearchBar(
            query = uiState.query,                    // 状态向下传递
            onQueryChange = viewModel::onQueryChanged, // 事件向上传递
            onSearch = viewModel::onSearch
        )
        // ...
    }
}

7.4 Compose 中的无状态 vs 有状态组件

这是组件设计的重要分类:

无状态组件(Stateless)

kotlin 复制代码
// 纯展示,所有数据通过参数传入
@Composable
fun UserProfile(
    name: String,            // 外部管理状态
    avatarUrl: String,
    isOnline: Boolean,
    onProfileClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Surface(
        modifier = modifier.clickable(onClick = onProfileClick)
    ) {
        Row {
            AsyncImage(model = avatarUrl, contentDescription = null)
            Column {
                Text(name)
                OnlineIndicator(isOnline)
            }
        }
    }
}

优点:完全可复用、可预测、易于测试。

有状态组件(Stateful)

kotlin 复制代码
// 内部持有状态 ------ 适用于简单的自包含组件
@Composable
fun ExpandableText(
    text: String,
    maxLines: Int = 3
) {
    var expanded by remember { mutableStateOf(false) }
    
    Column {
        Text(
            text = text,
            maxLines = if (expanded) Int.MAX_VALUE else maxLines
        )
        TextButton(onClick = { expanded = !expanded }) {
            Text(if (expanded) "Show less" else "Show more")
        }
    }
}

优点:使用方便,调用方无需关心内部状态。

设计原则

kotlin 复制代码
// 最佳实践:同时提供有状态和无状态两个版本
// 无状态版本
@Composable
fun LikeButton(
    isLiked: Boolean,
    onLikeChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier
) {
    IconButton(onClick = { onLikeChange(!isLiked) }) {
        Icon(
            imageVector = if (isLiked) Icons.Default.Favorite 
                          else Icons.Default.FavoriteBorder,
            contentDescription = "Like",
            tint = if (isLiked) Color.Red else Color.Gray
        )
    }
}

// 有状态包装
@Composable
fun LikeButton(
    initialLiked: Boolean = false,
    modifier: Modifier = Modifier
) {
    var isLiked by remember { mutableStateOf(initialLiked) }
    LikeButton(
        isLiked = isLiked,
        onLikeChange = { isLiked = it },
        modifier = modifier
    )
}

7.5 状态提升的层级决策

状态应该放在哪个层级?

决策流程图

7.6 实际案例:搜索页面

kotlin 复制代码
// 1. ViewModel ------ 所有状态的单一可信源
class ProductViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(ProductUiState())
    val uiState: StateFlow<ProductUiState> = _uiState.asStateFlow()

    fun onSearchQueryChanged(query: String) {
        _uiState.update { it.copy(searchQuery = query) }
    }

    fun onSearch() {
        val query = _uiState.value.searchQuery
        if (query.isBlank()) return
        _uiState.update { it.copy(isSearching = true) }
        viewModelScope.launch {
            try {
                val results = repository.search(query)
                _uiState.update { it.copy(results = results, isSearching = false) }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isSearching = false) }
            }
        }
    }

    fun onClearSearch() {
        _uiState.update { ProductUiState() }
    }
}

data class ProductUiState(
    val searchQuery: String = "",
    val results: List<Product> = emptyList(),
    val isSearching: Boolean = false,
    val error: String? = null
)

// 2. Screen ------ 单向数据流的消费端
@Composable
fun ProductScreen(viewModel: ProductViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Column(modifier = Modifier.fillMaxSize()) {
        // 状态向下
        SearchBar(
            query = uiState.searchQuery,
            // 事件向上
            onQueryChange = viewModel::onSearchQueryChanged,
            onSearch = viewModel::onSearch,
            onClear = viewModel::onClearSearch
        )

        when {
            uiState.isSearching -> LoadingIndicator()
            uiState.error != null -> ErrorView(uiState.error!!)
            else -> ProductList(uiState.results)
        }
    }
}

7.7 本章小结

概念 要点
状态提升 将状态移到调用方,通过参数传回
无状态组件 外部管理状态,内部只显示和回调
有状态组件 内部持有状态,适用于简单自包含场景
单向数据流 状态向下传、事件向上传
单一可信源 状态定义在一处,避免多份副本导致的数据不一致
层级决策 全局→页面→组件→子组件,逐级下放
相关推荐
xingpanvip2 小时前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
goldenrolan2 小时前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai
AC赳赳老秦3 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
kiros_wang6 小时前
Android 常见面试题
android
货拉拉技术6 小时前
Hook植入日志协助定位问题方案
android
FlightYe6 小时前
Android投屏MirrorCast全链路
android
Ehtan_Zheng6 小时前
Kotlin const val vs val:字节码、性能与隐藏陷阱详解
android·kotlin
墨狂之逸才6 小时前
Android TV 垃圾应用清理指南
android
源来猿往7 小时前
记ffmpeg-8.1.1 之Android库编译(window)
android·ffmpeg