/ Jetpack Compose  Android  状态管理  ViewModel  remember  LaunchedEffect  副作用  单向数据流 

Jetpack Compose状态管理深度解析:从remember到ViewModel的实战指南


封面

为什么状态管理是 Compose 的核心

Jetpack Compose 采用声明式 UI 范式,界面是状态的函数:UI = f(State)。这意味着只要状态发生变化,Compose 就会自动重新组合(Recompose)受影响的 Composable,而无需手动操作 View。

与传统命令式 UI(XML + View)相比,Compose 的状态管理更加集中和透明,但也带来了新的挑战:

  • 状态的归属问题:状态应该放在哪个 Composable 中?

  • 状态共享问题:多个 Composable 如何共享同一状态?

  • 副作用问题:网络请求、数据库操作等副作用如何处理?

  • 生命周期问题:状态如何在配置变更(如旋转屏幕)后存活?

理解并掌握这些问题,才能写出真正高质量的 Compose 代码。

remember 与 mutableStateOf:状态的基本单元

remembermutableStateOf 是 Compose 状态管理的两个基础 API,需要配合使用:

@Composable
fun Counter() {
    // remember 让状态在重组时保持,mutableStateOf 让 Compose 追踪变化
    var count by remember { mutableStateOf(0) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "计数:$count", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { count++ }) {
            Text("点击增加")
        }
    }
}

remember 的作用是在重组过程中记住计算结果。如果没有 remember,每次重组都会重新创建状态,导致状态丢失。mutableStateOf 创建一个可观察的状态容器,当其值变化时,所有读取该状态的 Composable 都会触发重组。

除了 mutableStateOf,Compose 还提供了其他状态类型:

  • mutableStateListOf:可观察的 List 状态

  • mutableStateMapOf:可观察的 Map 状态

  • derivedStateOf:从其他状态派生的只读状态,避免不必要的重组

  • snapshotFlow:将 Compose 状态转换为 Flow

状态提升(State Hoisting):单向数据流的实践

状态提升是 Compose 官方推荐的核心设计模式。其核心思想是:将状态从低层 Composable 移到高层 Composable,使低层组件成为无状态的(Stateless),只接收状态和事件回调。

// ❌ 有状态组件(不推荐单独使用)
@Composable
fun StatefulTextField() {
    var text by remember { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it })
}

// ✅ 无状态组件(推荐)
@Composable
fun StatelessTextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    TextField(
        value = value,
        onValueChange = onValueChange,
        modifier = modifier
    )
}

// ✅ 父组件持有状态(状态提升到父级)
@Composable
fun SearchScreen() {
    var searchQuery by remember { mutableStateOf("") }
    Column {
        StatelessTextField(
            value = searchQuery,
            onValueChange = { searchQuery = it }
        )
        Text("搜索:$searchQuery")
    }
}

状态提升的好处显而易见:无状态组件可以被复用、测试和预览,状态的流动方向清晰(从上到下),事件的流动方向明确(从下到上),这就是 Compose 的单向数据流(UDF)模式。

ViewModel 与 Compose 集成:跨越配置变更

remember 中的状态会在 Activity/Fragment 重建(如屏幕旋转)时丢失。对于需要持久化的业务状态,应该使用 ViewModel。

// ViewModel 定义
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(name = name) }
    }

    fun loadUser(userId: String) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val user = userRepository.getUser(userId)
                _uiState.update { it.copy(user = user, isLoading = false) }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }
}

data class UserUiState(
    val user: User? = null,
    val name: String = "",
    val isLoading: Boolean = false,
    val error: String? = null
)

// Composable 使用 ViewModel
@Composable
fun UserScreen(
    viewModel: UserViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> CircularProgressIndicator()
        uiState.error != null -> Text("错误:${uiState.error}")
        else -> UserContent(
            user = uiState.user,
            name = uiState.name,
            onNameChange = viewModel::updateName
        )
    }
}

注意这里使用了 collectAsStateWithLifecycle() 而不是 collectAsState()。前者在应用退到后台时会自动停止收集,节省资源,是 Google 推荐的方式(需要 lifecycle-runtime-compose 依赖)。

副作用处理:LaunchedEffect、SideEffect 与 DisposableEffect

Composable 函数应该是纯函数(无副作用),但实际开发中不可避免地需要执行副作用操作。Compose 提供了专门的副作用 API 来处理这些场景。

LaunchedEffect:在 Composable 内部启动协程,随 key 变化重启。

@Composable
fun ChatScreen(roomId: String) {
    val messages = remember { mutableStateListOf<Message>() }

    // 当 roomId 变化时,重新订阅消息
    LaunchedEffect(roomId) {
        chatRepository.observeMessages(roomId).collect { message ->
            messages.add(message)
        }
    }

    LazyColumn {
        items(messages) { message ->
            MessageItem(message)
        }
    }
}

DisposableEffect:需要清理的副作用(如注册/注销监听器)。

@Composable
fun LocationTracker(onLocationChanged: (Location) -> Unit) {
    val context = LocalContext.current

    DisposableEffect(Unit) {
        val locationManager = context.getSystemService(LocationManager::class.java)
        val listener = LocationListener { location ->
            onLocationChanged(location)
        }
        locationManager.requestLocationUpdates(GPS_PROVIDER, 1000L, 0f, listener)

        // onDispose 在 Composable 离开组合时执行
        onDispose {
            locationManager.removeUpdates(listener)
        }
    }
}

副作用 API 总结:

  • LaunchedEffect(key):启动协程,key 变化时重启,适合数据加载、订阅流

  • DisposableEffect(key):有清理逻辑的副作用,适合注册/注销监听器

  • SideEffect:每次成功重组后执行,适合同步 Compose 状态到非 Compose 代码

  • rememberCoroutineScope():获取 Composable 绑定的协程作用域,用于事件触发的协程

  • produceState:将非 Compose 数据源转换为 Compose State

掌握这些 API,你就能在 Compose 的"纯函数世界"中优雅地处理各种副作用,写出既正确又高效的 Android 应用。

发布评论

热门评论区: