compose-multiplatform-file-picker
compose-multiplatform-file-picker copied to clipboard
Exception on Android
I have code that's working fine in other Compose clients but on Android I'm getting following when calling getFileByteArray
01-14 19:37:45.657 22788 22788 E AndroidRuntime: java.lang.IllegalArgumentException: Uri lacks 'file' scheme: content://com.android.providers.media.documents/document/image%3A1000050843
01-14 19:37:45.657 22788 22788 E AndroidRuntime: at androidx.core.net.UriKt.toFile(Uri.kt:43)
01-14 19:37:45.657 22788 22788 E AndroidRuntime: at com.darkrockstudios.libraries.mpfilepicker.AndroidFile.getFileByteArray(AndroidFilePicker.kt:15)
following is code I have
val coroutineScope = rememberCoroutineScope()
val fileExtensions = listOf("jpg", "png")
FilePicker(show = show, fileExtensions = fileExtensions) { file ->
coroutineScope.launch {
val data = file?.getFileByteArray()
data?.let {
....
}
}
}
I had the same problem and found a weird workaround (inspired by https://github.com/Wavesonics/compose-multiplatform-file-picker/pull/104)
I have a global variable in commainMain to store the byte array (this is not necessary, only if you want to do more than just displaying the picked image):
// this variable is set from "outside" in the jvmMain and androidMain
var imageBytes : ByteArray = byteArrayOf()
this is my commonMain part:
var showFilePicker by remember { mutableStateOf(false) }
colorButton(onClick = {showFilePicker = true}, text = "Choose Image")
var showImage by remember {mutableStateOf(false)}
val fileType = listOf("jpg", "png")
var file by remember { mutableStateOf<MPFile<Any>?>(null) }
FilePicker(
show = showFilePicker,
fileExtensions = fileType,
onFileSelected = {
showFilePicker = false
scope.launch {
file = it
showImage = true
}
}
)
if (showImage) {
ImageFromFile(file)
}
then in commonMain i have this expect function declared:
@Composable
expect fun ImageFromFile(file: MPFile<Any>?)
The actual implementation in androidMain:
@Composable
actual fun ImageFromFile(file: MPFile<Any>?) {
var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
if (file != null) {
val uri = Uri.parse(file.path)
val stream = LocalContext.current.contentResolver.openInputStream(uri)
if (stream != null) {
val bytes = stream.readBytes()
imageBytes = bytes // sets the global byteArray variable in commonMain
stream.close()
if (bytes.isNotEmpty()) {
imageBitmap.prepareToDraw()
imageBitmap = BitmapFactory.decodeByteArray(
bytes,
0,
bytes.size
).asImageBitmap()
}
}
}
Image(
bitmap = imageBitmap,
""
)
}
actual implementation in jvmMain:
@Composable
actual fun ImageFromFile(file: MPFile<Any>?) {
var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
val scope = rememberCoroutineScope()
scope.launch {
if (file == null) return@launch
imageBitmap.prepareToDraw()
imageBytes = file.getFileByteArray() // sets the global byteArray variable in commonMain
imageBitmap = Image.makeFromEncoded(file.getFileByteArray()).toComposeImageBitmap()
}
Image(
bitmap = imageBitmap,
""
)
}
Furthermore, I have these functions for displaying an Image from just a byte Array:
// commonMain expect function
@Composable
expect fun ImageFromByteArray(byteArray: ByteArray, modifier:Modifier = Modifier, scale: ContentScale = ContentScale.Fit)
// androidMain actual function
@Composable
actual fun ImageFromByteArray(byteArray: ByteArray, modifier: Modifier, scale: ContentScale) {
var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
imageBitmap = BitmapFactory.decodeByteArray(byteArray,
0,
byteArray.size).asImageBitmap()
Image(
modifier = modifier,
bitmap = imageBitmap,
contentDescription = "",
contentScale = scale
)
}
// jvmMain actual function
@Composable
actual fun ImageFromByteArray(byteArray: ByteArray, modifier: Modifier, scale: ContentScale) {
val scope = rememberCoroutineScope()
var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
scope.launch {
imageBitmap.prepareToDraw()
imageBitmap = Image.makeFromEncoded(byteArray).toComposeImageBitmap()
}
Image(
modifier = modifier,
bitmap = imageBitmap,
contentDescription = "",
contentScale = scale
)
}
Thanks @lusc8520 ....using adapted version of that now in https://github.com/joreilly/GeminiKMP/blob/main/composeApp/src/androidMain/kotlin/actual.kt
Could you try this?: https://stackoverflow.com/a/8370299/9133703
While folks are waiting on that pr, here's some of the guts of it worked as an extension function so that you can use the library as is. This just steals the main punchline from vinceglb's pr of using contentResolver to turn a Uri into a ByteArray. Assumes the original works correctly in iOS. Handling the non-null assertion and supplying the Android context is dealt with elsewhere.
Common:
expect suspend fun MPFile<Any>.getBytes(): ByteArray
iOS:
actual suspend fun MPFile<Any>.getBytes(): ByteArray { return this.getFileByteArray() }
Android:
actual suspend fun MPFile<Any>.getBytes(): ByteArray { return AndroidApplication.getApplicationContext().contentResolver.openInputStream(this.platformFile as Uri).use { stream -> stream!!.readBytes() } }
Thanks @randyheaton its works with your workaround.
@vinceglb Did you know if the PR is mergeable ?
@c4software I sent a message to other maintainers, I'll let you know as soon as I have any news 👍
@c4software I discuss with Wavesonics, it supposed to have a reviewer before merging. So, I'm waiting to a maintainer to review it. If it takes too long, I'll check with Wavesonics how to proceed.
Thanks for you feedback 👍