You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose Multiplatform:Voyager页面传参与ImagePicker结合底部导航问题

Voyager 导航:向Screen传参数及ImagePicker与底部导航结合问题

我正在学习Voyager相关教程,有两个问题需要解决:

  1. 能否从App组件向Voyager Screen传递参数?如果可以,该怎么实现?
  2. 如何把当前实现的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传递:

    1. 在Koin模块中配置ImagePicker实例:
      val appModule = module {
          single { ImagePickerFactory().createPicker() }
      }
      
    2. 在ProfileViewModel中注入:
      class ProfileViewModel(private val imagePicker: ImagePicker) : ScreenModel {
          fun handleImagePick() = imagePicker.ShowImagePicker()
          fun saveImage(bytes: ByteArray) { /* 保存逻辑 */ }
      }
      
    3. ProfileScreen不再需要接收参数:
      class ProfileScreen : Screen {
          @Composable
          override fun Content() {
              val viewModel = getScreenModel<ProfileViewModel>()
              viewModel.imagePicker.RegisterPicker { viewModel.saveImage(it) }
              ProfileScreenRoot(viewModel) { viewModel.handleImagePick() }
          }
      }
      
  • 方案二:传递可序列化参数
    如果必须直接传参,只传递可序列化数据(比如图片类型字符串),而不是ImagePicker实例,在Screen内部创建/获取ImagePicker。

2. ImagePicker与底部导航结合的正确姿势

核心是避免将持有Activity的ImagePicker传递给Tab,改用CompositionLocal在Compose树中传递:

  1. 定义全局CompositionLocal:
    val LocalImagePicker = staticCompositionLocalOf<ImagePicker> {
        error("ImagePicker未在Composition树中提供")
    }
    
  2. 在App Composable中提供实例:
    @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() }
                    )
                }
            }
        }
    }
    
  3. 在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() }
        }
    }
    
  4. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 15:54:51