Compose Multiplatform:Voyager页面传参与ImagePicker结合底部导航问题
Voyager 导航:向Screen传参数及ImagePicker与底部导航结合问题
我正在学习Voyager相关教程,有两个问题需要解决:
- 能否从App组件向Voyager Screen传递参数?如果可以,该怎么实现?
- 如何把当前实现的
ImagePicker和底部导航结合起来?
尝试多种方法后都失败了,要么页面空白,要么触发如下崩溃:
java.lang.RuntimeException: Parcelable encountered IOException writing serializable object
Android端ImagePicker实现
actual class ImagePickerFactory { @Composable actual fun createPicker(): ImagePicker { val activity = LocalContext.current as ComponentActivity return remember(activity) { ImagePicker(activity) } } } actual class ImagePicker(private val activity: ComponentActivity) { private lateinit var getContent: ActivityResultLauncher<String> @Composable actual fun RegisterPicker(onImagePicked: (ByteArray) -> Unit) { getContent = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { activity.contentResolver.openInputStream(uri)?.use { onImagePicked(it.readBytes()) } } } } actual fun ShowImagePicker() { getContent.launch("image/*") } }
共享模块代码
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { App(ImagePickerFactory().createPicker()) } } } @Composable @Preview fun App(imagePicker: ImagePicker) { initKoin() MaterialTheme { TabNavigator( tab = HomeTab ) { Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { BottomNavigation( backgroundColor = Color(0xFF3F54BE) ) { TabNavigationItem(HomeTab) TabNavigationItem(SearchTab) TabNavigationItem(ProfileTab(imagePicker)) } }, content = { CurrentTab() } ) } } } @Composable private fun RowScope.TabNavigationItem(tab: Tab) { val tabNavigator: TabNavigator = LocalTabNavigator.current BottomNavigationItem( modifier = Modifier.padding(top = 24.dp), selected = tabNavigator.current == tab, onClick = { tabNavigator.current = tab }, icon = { tab.options.icon?.let { icon -> Icon( painter = icon, contentDescription = null, tint = if (tabNavigator.current == tab) Color.White else Color.Gray ) } }, label = { Text(text = tab.options.title) } ) } data class ProfileTab(private val imagePicker: ImagePicker) : Tab { @Composable override fun Content() { Navigator(screen = ProfileScreen(imagePicker)) { navigator -> SlideTransition(navigator = navigator) } } override val options: TabOptions @Composable get() { val index: UShort = 2u return TabOptions( icon = rememberVectorPainter(Icons.Default.Person), index = index, title = "" ) } } class ProfileScreen(private val imagePicker: ImagePicker) : Screen { @Composable override fun Content() { val viewModel = getScreenModel<ProfileViewModel>() imagePicker.RegisterPicker { imageBytes -> viewModel.saveUserImage(imageBytes) } ProfileScreenRoot(viewModel) { imagePicker.ShowImagePicker() } } } @Composable fun ProfileScreenRoot(viewModel: ProfileViewModel, onImagePicked: () -> Unit) { //code unrelated to this issue }
问题分析与解决方案
崩溃根源
你遇到的序列化崩溃,是因为ImagePicker持有ComponentActivity引用,而Voyager会对Tab和Screen进行序列化以保存状态,ComponentActivity属于不可序列化对象,直接传递会触发异常。
1. 向Voyager Screen传递参数的正确方式
Voyager支持传参,但必须传递可序列化对象(基本类型、实现Serializable/Parcelable的data class),或通过依赖注入/CompositionLocal传递不可序列化对象:
方案一:依赖注入(推荐)
用Koin将ImagePicker注入到ProfileViewModel,避免直接向Screen/Tab传递:- 在Koin模块中配置
ImagePicker实例:val appModule = module { single { ImagePickerFactory().createPicker() } } - 在
ProfileViewModel中注入:class ProfileViewModel(private val imagePicker: ImagePicker) : ScreenModel { fun handleImagePick() = imagePicker.ShowImagePicker() fun saveImage(bytes: ByteArray) { /* 保存逻辑 */ } } ProfileScreen不再需要接收参数:class ProfileScreen : Screen { @Composable override fun Content() { val viewModel = getScreenModel<ProfileViewModel>() viewModel.imagePicker.RegisterPicker { viewModel.saveImage(it) } ProfileScreenRoot(viewModel) { viewModel.handleImagePick() } } }
- 在Koin模块中配置
方案二:传递可序列化参数
如果必须直接传参,只传递可序列化数据(比如图片类型字符串),而不是ImagePicker实例,在Screen内部创建/获取ImagePicker。
2. ImagePicker与底部导航结合的正确姿势
核心是避免将持有Activity的ImagePicker传递给Tab,改用CompositionLocal在Compose树中传递:
- 定义全局CompositionLocal:
val LocalImagePicker = staticCompositionLocalOf<ImagePicker> { error("ImagePicker未在Composition树中提供") } - 在
AppComposable中提供实例:@Composable fun App() { initKoin() val imagePicker = ImagePickerFactory().createPicker() CompositionLocalProvider(LocalImagePicker provides imagePicker) { MaterialTheme { TabNavigator(tab = HomeTab) { Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { BottomNavigation(backgroundColor = Color(0xFF3F54BE)) { TabNavigationItem(HomeTab) TabNavigationItem(SearchTab) TabNavigationItem(ProfileTab) // 不再传参 } }, content = { CurrentTab() } ) } } } } - 在
ProfileScreen中获取实例:class ProfileScreen : Screen { @Composable override fun Content() { val imagePicker = LocalImagePicker.current val viewModel = getScreenModel<ProfileViewModel>() imagePicker.RegisterPicker { viewModel.saveUserImage(it) } ProfileScreenRoot(viewModel) { imagePicker.ShowImagePicker() } } } - 修改
MainActivity,不再传递参数:class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { App() } } }
额外注意事项
rememberLauncherForActivityResult必须在稳定的Composable位置调用,确保ImagePicker用remember持有,避免重组时重复创建。- Voyager的
Tab和Screen会被序列化,绝对不要让它们持有Context、Activity等不可序列化对象。
内容的提问来源于stack exchange,提问作者Naum Djordjevic
相关产品推荐
相关产品推荐

