Article / 2026/06/16

Composable_数据传递

1. [概述](概述) 2. [参数传递(Props Down)](参数传递propsdown) 3. [状态提升(State Hoisting)](状态提升statehoisting) 4. [CompositionLocal](c...

Jetpack Compose 数据传递详解

目录

  1. 概述
  2. 参数传递(Props Down)
  3. 状态提升(State Hoisting)
  4. CompositionLocal
  5. ViewModel 共享状态
  6. Lambda 回调(Events Up)
  7. 导航传参(Navigation)
  8. SharedFlow / StateFlow
  9. 数据传递模式对比
  10. 最佳实践

概述

Jetpack Compose 采用**单向数据流(Unidirectional Data Flow,UDF)**架构:

State  ──▶  UI(Composable)


           Events  ──▶  ViewModel / State Holder
  • 数据向下流动:父 Composable 将数据以参数形式传给子 Composable
  • 事件向上传递:子 Composable 通过 Lambda 回调将用户事件传给父级

参数传递(Props Down)

基本参数传递

最简单的方式:父 Composable 直接将数据作为参数传入子 Composable。

// 子 Composable
@Composable
fun UserCard(
    name: String,
    age: Int,
    avatarUrl: String,
    modifier: Modifier = Modifier
) {
    Card(modifier = modifier) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = name, style = MaterialTheme.typography.titleMedium)
            Text(text = "Age: $age")
        }
    }
}

// 父 Composable
@Composable
fun ParentScreen() {
    UserCard(
        name = "张三",
        age = 28,
        avatarUrl = "https://example.com/avatar.jpg"
    )
}

传递数据类(Data Class)

推荐将相关属性封装为数据类,减少参数数量:

data class User(
    val id: Long,
    val name: String,
    val age: Int,
    val avatarUrl: String
)

@Composable
fun UserCard(user: User, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Text(text = user.name)
        Text(text = "Age: ${user.age}")
    }
}

@Composable
fun ParentScreen() {
    val user = User(id = 1L, name = "张三", age = 28, avatarUrl = "")
    UserCard(user = user)
}

传递 List / 集合

@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users, key = { it.id }) { user ->
            UserCard(user = user)
        }
    }
}

状态提升(State Hoisting)

概念

将状态从子 Composable “提升”到父 Composable,使子 Composable 变为无状态(Stateless),由父级持有并管理状态。

模式:

  • 子组件接收 value 参数(状态值)
  • 子组件接收 onValueChange 回调(触发状态变更)

示例:TextField 状态提升

// ❌ 有状态(不推荐复用)
@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 LoginScreen() {
    var username by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column {
        StatelessTextField(
            value = username,
            onValueChange = { username = it }
        )
        StatelessTextField(
            value = password,
            onValueChange = { password = it }
        )
        Button(onClick = { /* 使用 username / password 登录 */ }) {
            Text("登录")
        }
    }
}

提升层级原则

状态应提升到所有需要读/写该状态的 Composable 的最低公共祖先

          Screen(持有 selectedTab 状态)
           /                \
    TabRow(读取)       TabContent(读取)

CompositionLocal

概念

CompositionLocal 用于在 Composable 树中隐式传递数据,避免逐层手动传参(prop drilling)。适用于主题、语言、用户配置等全局性数据。

内置 CompositionLocal 示例

// MaterialTheme 本质是 CompositionLocal
@Composable
fun MyText() {
    // 直接获取当前主题颜色,无需父级传参
    val color = MaterialTheme.colorScheme.primary
    Text(text = "Hello", color = color)
}

自定义 CompositionLocal

方式一:compositionLocalOf(值变化时,仅读取该值的子树重组)

// 定义
val LocalAppConfig = compositionLocalOf<AppConfig> {
    error("No AppConfig provided")
}

data class AppConfig(
    val apiBaseUrl: String,
    val debugMode: Boolean
)

// 提供(在顶层 Composable)
@Composable
fun App() {
    val config = AppConfig(
        apiBaseUrl = "https://api.example.com",
        debugMode = BuildConfig.DEBUG
    )
    CompositionLocalProvider(LocalAppConfig provides config) {
        MainScreen()
    }
}

// 消费(任意深度子 Composable)
@Composable
fun SettingsScreen() {
    val config = LocalAppConfig.current
    Text(text = "API: ${config.apiBaseUrl}")
}

方式二:staticCompositionLocalOf(值变化时,整棵子树全部重组,适合极少变化的值)

val LocalNavigator = staticCompositionLocalOf<NavController> {
    error("No NavController provided")
}

CompositionLocal vs 参数传递

场景推荐方式
业务数据(用户信息、列表数据)参数传递
全局配置(主题、语言、权限)CompositionLocal
频繁变化的状态参数传递 + State Hoisting
极少变化的全局对象(NavController)staticCompositionLocalOf

ViewModel 共享状态

概念

多个 Composable 通过共同的 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: Long) {
        viewModelScope.launch {
            val user = userRepository.getUser(userId)
            _uiState.update { it.copy(user = user, isLoading = false) }
        }
    }
}

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

// Screen(收集 ViewModel 状态)
@Composable
fun UserScreen(
    viewModel: UserViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    if (uiState.isLoading) {
        CircularProgressIndicator()
    } else {
        UserContent(
            uiState = uiState,
            onNameChange = viewModel::updateName
        )
    }
}

// 子 Composable(无状态)
@Composable
fun UserContent(
    uiState: UserUiState,
    onNameChange: (String) -> Unit
) {
    Column {
        uiState.user?.let { Text(text = it.name) }
        TextField(
            value = uiState.name,
            onValueChange = onNameChange
        )
    }
}

在 Navigation 中共享 ViewModel

// 同一 NavGraph 内的多个 Screen 共享同一个 ViewModel
@Composable
fun NavGraph(navController: NavHostController) {
    NavHost(navController, startDestination = "step1") {
        navigation(startDestination = "step1", route = "wizard") {
            composable("step1") { backStackEntry ->
                val parentEntry = remember(backStackEntry) {
                    navController.getBackStackEntry("wizard")
                }
                val viewModel: WizardViewModel = viewModel(parentEntry)
                Step1Screen(viewModel)
            }
            composable("step2") { backStackEntry ->
                val parentEntry = remember(backStackEntry) {
                    navController.getBackStackEntry("wizard")
                }
                val viewModel: WizardViewModel = viewModel(parentEntry)
                Step2Screen(viewModel)
            }
        }
    }
}

Lambda 回调(Events Up)

基本回调

@Composable
fun ActionButton(
    text: String,
    onClick: () -> Unit,        // 无参回调
    modifier: Modifier = Modifier
) {
    Button(onClick = onClick, modifier = modifier) {
        Text(text)
    }
}

// 父级使用
@Composable
fun ParentScreen() {
    ActionButton(
        text = "提交",
        onClick = { /* 处理点击 */ }
    )
}

带参数的回调

@Composable
fun ItemList(
    items: List<Item>,
    onItemClick: (Item) -> Unit,      // 传递点击的 Item
    onItemDelete: (Long) -> Unit       // 传递 Item ID
) {
    LazyColumn {
        items(items, key = { it.id }) { item ->
            ItemRow(
                item = item,
                onClick = { onItemClick(item) },
                onDelete = { onItemDelete(item.id) }
            )
        }
    }
}

事件密封类(推荐用于复杂 UI)

将多个事件统一为密封类,减少回调参数数量:

sealed class LoginEvent {
    data class UsernameChanged(val value: String) : LoginEvent()
    data class PasswordChanged(val value: String) : LoginEvent()
    object LoginClicked : LoginEvent()
    object ForgotPasswordClicked : LoginEvent()
}

@Composable
fun LoginScreen(
    state: LoginUiState,
    onEvent: (LoginEvent) -> Unit
) {
    Column {
        TextField(
            value = state.username,
            onValueChange = { onEvent(LoginEvent.UsernameChanged(it)) }
        )
        TextField(
            value = state.password,
            onValueChange = { onEvent(LoginEvent.PasswordChanged(it)) }
        )
        Button(onClick = { onEvent(LoginEvent.LoginClicked) }) {
            Text("登录")
        }
        TextButton(onClick = { onEvent(LoginEvent.ForgotPasswordClicked) }) {
            Text("忘记密码?")
        }
    }
}

// ViewModel 处理
class LoginViewModel : ViewModel() {
    fun onEvent(event: LoginEvent) {
        when (event) {
            is LoginEvent.UsernameChanged -> updateUsername(event.value)
            is LoginEvent.PasswordChanged -> updatePassword(event.value)
            LoginEvent.LoginClicked -> login()
            LoginEvent.ForgotPasswordClicked -> navigateToForgotPassword()
        }
    }
}

导航传参(Navigation)

基本路由传参

// 定义路由
const val ROUTE_USER_DETAIL = "user/{userId}"

// 导航时传参
navController.navigate("user/42")

// 接收参数
composable(
    route = "user/{userId}",
    arguments = listOf(navArgument("userId") { type = NavType.LongType })
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getLong("userId") ?: return@composable
    UserDetailScreen(userId = userId)
}

可选参数

// ?key=value 格式
composable(
    route = "search?query={query}",
    arguments = listOf(
        navArgument("query") {
            type = NavType.StringType
            nullable = true
            defaultValue = null
        }
    )
) { backStackEntry ->
    val query = backStackEntry.arguments?.getString("query")
    SearchScreen(initialQuery = query)
}

// 导航(可不传)
navController.navigate("search")
navController.navigate("search?query=keyword")

复杂对象传递(序列化为 JSON)

// 添加 Gson 或 kotlinx.serialization
// build.gradle: implementation "com.google.code.gson:gson:2.10.1"

// 编码
val userJson = Uri.encode(Gson().toJson(user))
navController.navigate("detail/$userJson")

// 解码
composable(
    route = "detail/{userJson}",
    arguments = listOf(navArgument("userJson") { type = NavType.StringType })
) { backStackEntry ->
    val json = backStackEntry.arguments?.getString("userJson") ?: ""
    val user = Gson().fromJson(json, User::class.java)
    DetailScreen(user = user)
}

注意:导航传复杂对象建议只传 ID,通过 ViewModel 从数据源重新加载,避免数据过大和序列化问题。

返回结果(Back Stack Result)

// 子屏幕:将结果存入 SavedStateHandle
navController.previousBackStackEntry
    ?.savedStateHandle
    ?.set("selected_city", "北京")
navController.popBackStack()

// 父屏幕:观察结果
@Composable
fun ParentScreen(navController: NavController) {
    val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
    val selectedCity by savedStateHandle
        ?.getStateFlow("selected_city", "")
        ?.collectAsStateWithLifecycle() ?: return

    Text(text = "选择的城市:$selectedCity")
}

SharedFlow / StateFlow

StateFlow(状态)

适合持久状态,新订阅者会立即收到最新值:

class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() { _count.update { it + 1 } }
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val count by viewModel.count.collectAsStateWithLifecycle()
    Column {
        Text("Count: $count")
        Button(onClick = viewModel::increment) { Text("+1") }
    }
}

SharedFlow(事件/一次性副作用)

适合导航、Toast、Dialog 等一次性事件

class LoginViewModel : ViewModel() {
    private val _events = MutableSharedFlow<LoginSideEffect>()
    val events: SharedFlow<LoginSideEffect> = _events.asSharedFlow()

    fun login(username: String, password: String) {
        viewModelScope.launch {
            val result = authRepository.login(username, password)
            if (result.isSuccess) {
                _events.emit(LoginSideEffect.NavigateToHome)
            } else {
                _events.emit(LoginSideEffect.ShowError("登录失败,请重试"))
            }
        }
    }
}

sealed class LoginSideEffect {
    object NavigateToHome : LoginSideEffect()
    data class ShowError(val message: String) : LoginSideEffect()
}

// 在 Composable 中收集一次性事件
@Composable
fun LoginScreen(
    viewModel: LoginViewModel = viewModel(),
    onNavigateToHome: () -> Unit
) {
    val context = LocalContext.current

    LaunchedEffect(Unit) {
        viewModel.events.collect { effect ->
            when (effect) {
                LoginSideEffect.NavigateToHome -> onNavigateToHome()
                is LoginSideEffect.ShowError -> {
                    Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    // UI...
}

数据传递模式对比

传递方式适用场景优点缺点
参数传递父→子直接传数据简单直观、类型安全层级深时繁琐(prop drilling)
状态提升兄弟组件共享状态无状态子组件易测试状态管理上移,父级变复杂
CompositionLocal主题、全局配置避免 prop drilling隐式依赖,不适合业务数据
ViewModel跨屏幕、复杂业务逻辑生命周期感知、可测试需要引入 DI 或 viewModel()
Lambda 回调子→父传事件解耦,符合 UDF过多回调时参数列表膨胀
事件密封类复杂 UI 事件集中管理事件,易扩展需要额外定义密封类
Navigation 参数屏幕间传递简单值系统支持、深链接兼容不适合传大型对象
StateFlow/SharedFlow异步状态/一次性事件生命周期安全需要理解 Flow 机制

最佳实践

1. 遵循单向数据流

ViewModel(State) ──▶ Composable(展示) ──▶ ViewModel(Event处理)

2. Screen 与内容 Composable 分离

// ✅ Screen 负责获取数据
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    UserContent(state = state, onEvent = viewModel::onEvent)
}

// ✅ Content 无状态,仅负责渲染
@Composable
fun UserContent(state: UserUiState, onEvent: (UserEvent) -> Unit) {
    // 纯 UI
}

3. 使用 key 稳定 List 重组

LazyColumn {
    items(users, key = { it.id }) { user ->
        UserItem(user = user)
    }
}

4. 避免在 Composable 中直接持有可变状态的引用

// ❌ 错误:直接读取 ViewModel 内部 MutableState
val text = viewModel.mutableText

// ✅ 正确:通过 StateFlow/collectAsState 订阅
val text by viewModel.text.collectAsStateWithLifecycle()

5. rememberrememberSaveable

// remember:重组期间保持,屏幕旋转后丢失
var count by remember { mutableStateOf(0) }

// rememberSaveable:重组 + 配置变更(旋转)均保持
var count by rememberSaveable { mutableStateOf(0) }

// 复杂对象使用 Saver
var user by rememberSaveable(stateSaver = UserSaver) { mutableStateOf(User()) }

6. 副作用处理

@Composable
fun MyScreen(userId: Long, viewModel: MyViewModel = viewModel()) {
    // userId 变化时重新加载
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    // 只执行一次(进入组合时)
    LaunchedEffect(Unit) {
        viewModel.trackScreenView()
    }
}

总结

简单父子传参     ──▶  函数参数(Parameter)
子→父通信       ──▶  Lambda 回调
避免逐层传参    ──▶  CompositionLocal(谨慎使用)
跨组件共享状态  ──▶  ViewModel + StateFlow
一次性事件      ──▶  ViewModel + SharedFlow
屏幕间跳转传值  ──▶  Navigation 参数(简单值)或 ViewModel ID + 数据层

Compose 数据传递的核心原则:状态下传,事件上报,保持单向数据流

Giscus 未启用:请在 src/site.config.ts 中配置 repoId 与 categoryId。