android-jetpack-composekotlin-multiplatformphoto-gallerycompose-multiplatform

Compose multiplatform Voyager navigation with ImagePicker


I was watching this tutorial and I was wondering if there's a way to send parameters from the App to a Voyager Screen and if so, how? Or to somehow combine this ImagePicker implementation with bottom navigation?

I've tried many things but nothing actually worked, I've either ended up with a blank screen or a crash that said

java.lang.RuntimeException: Parcelable encountered IOException writing serializable object

Code:

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/*")
    }
}

These are my image picker classes on android side. Shared code looks like this:

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
}

Solution

  • I've end up "fixing" this by replacing the ImagePicker with this cool library that is used for both Android and iOS called peekaboo. It's pretty simple and neat, I recommend using it instead of implementing image picker separately on both platforms, because it's much easier and you won't get into issues like needing to pass context on android side for creating the image picker factory, passing it to the screens from App, etc.