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

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 本章小结

概念 要点
状态提升 将状态移到调用方,通过参数传回
无状态组件 外部管理状态,内部只显示和回调
有状态组件 内部持有状态,适用于简单自包含场景
单向数据流 状态向下传、事件向上传
单一可信源 状态定义在一处,避免多份副本导致的数据不一致
层级决策 全局→页面→组件→子组件,逐级下放
相关推荐
传奇开心果编程20 分钟前
【Flutter入门练中学】第11课:状态管理进阶与声明式路由
android·学习·flutter·ui·ios
JasonSJX1 小时前
四端自建播放器怎么落地:Android、iOS、tvOS、Tizen 的 DRM 播放 SDK 技术盘点
android·ios·音视频·视频防录屏·加密保护课程·直播安全
驰骋工作流1 小时前
工作流引擎四大流程模块功能点统计:769 项能力清单梳理低代码工作流引擎表单
android·低代码·rxjava
恋猫de小郭1 小时前
Flutter 多窗口重要优化合并,多窗口性能和实用性大幅提升
android·前端·flutter
AirDroid_cn1 小时前
实时定位孩子手机超便捷!iPhone家长同步收提醒,省心又踏实
android
hai_android2 小时前
LruCache 图片浏览器内存缓存
android·java·kotlin
今天要早睡_3 小时前
C++入门到精通:类和对象(下)全方位解析
android·c++
Godikov7 小时前
在 Android 上跑 Kafka Consumer:一个实时定位数据接入的踩坑记录
android
mmsx7 小时前
Android 自定义 View 三关:触摸分发、绘制与坐标错位
android·kotlin
JMchen7 小时前
自定义View在复杂业务场景中的实战
android·kotlin·canvas