https://kotlinlang.org logo
#compose-ios
Title
# compose-ios
m

Max

03/28/2024, 9:41 PM
Hey! Does anyone have an idea how I can display an image in Compose coming from the file system? I built a contact importer with expect/actual - it works. Now On Android I have for example an URI pointing to the contact image. How can i display this uri in common compose now?
m

Matthew Feinberg

03/28/2024, 11:15 PM
There might be an easier way, but I solved this by using expect/actual to make an ImageBitmap extension... For example (not actually how I did it; see below for my actual solution)...
Copy code
expect fun ImageBitmap.Companion.fromPlatformFile(fileReference: String) : Result<ImageBitmap>
Here,
fileReference
could be a path, URI, whatever you need. For better type safety, you could encapsulate this in another class like
PlatformFile
or something (which could be returned by your contact importer). Used something like...
Copy code
val contact = getContact() // whatever your expect/actual function is.

val bitmap = ImageBitmap.fromPlatformFile(contact.imageFile).getOrNull()
In my case, what I actually did was to make an expect/actual ImageBitmap extension that makes an image from a ByteArray:
Copy code
expect fun ImageBitmap.Companion.fromByteArray(data: ByteArray) : ImageBitmap?

// Apple
actual fun ImageBitmap.Companion.fromByteArray(data: ByteArray): ImageBitmap? =
    Image.makeFromEncoded(data).toComposeImageBitmap()

// Android
actual fun ImageBitmap.Companion.fromByteArray(data: ByteArray): ImageBitmap? = BitmapFactory.decodeByteArray(data,0,data.size)?.asImageBitmap()
And I have a
PlatformFile
class that wraps a platform-dependent reference to a file (URI, path, whatever) and provides an expect/actual
.readBytes() : Result<ByteArray>
method. So you end up with something like:
Copy code
getContact().imageFile.readBytes()
   .map { ImageBitmap.fromByteArray(it) }
   .getOrNull()
Of course, that loads the image synchronously; you'll need to do some stuff with coroutines so it doesn't block the main thread...
m

Michael Paus

03/29/2024, 9:54 AM
I don’t understand why you do not just use https://github.com/Kotlin/kotlinx-io (or https://github.com/square/okio) for the IO stuff.
m

Matthew Feinberg

03/29/2024, 9:59 AM
Does okio support URIs? In their docs it says:
Okio’s
Path
class supports Windows-style (like
C:\autoexec.bat
) and UNIX-style paths (like
/etc/passwd
).
...but on Android, you quite often get
content://
URIs that need to be read via
ContentResolver
....
m

Michael Paus

03/29/2024, 6:09 PM
What about:
Copy code
fun getSource(uri: Uri): Source {
    val contentResolver = activity.getContentResolver()
    val inputStream = contentResolver.openInputStream(uri)
    if (inputStream == null) throw FileNotFoundException("Can't open input stream, uri: $uri")
    return inputStream.source()
}
I haven’t tested it though.
m

Matthew Feinberg

03/29/2024, 7:51 PM
But in this case you're still using expect/actual, though, so what's the benefit of using okio here?
And a
Uri
is not multiplatform... so in either case you're still going to need to use a string or have your own encapsulation for a platform type...
m

Michael Paus

03/30/2024, 6:42 AM
You are right with the URI. That should be replaced by a String. The benefit is that your IO-handling is now consistent, assuming that you use Okio elsewhere too.
m

Max

03/30/2024, 8:01 AM
@Matthew Feinberg thx for sharing your way, i will try it out, exactly what i was looking for. My fallback would have been uploading the contact pics on native side to firebase and then showing remote pictures in CMP, but that would be suboptimal.
m

Matthew Feinberg

03/31/2024, 12:40 AM
> assuming that you use Okio elsewhere too Ah, not sure about Max's needs, but in my case literally the only place where I do I/O is reading user-selected images from the Gallery/Camera Roll. So adding another dependency would be overkill. I'm hoping by the time I have proper I/O needs, kotlinx-io's
kotlinx.io.files
will have moved beyond the experimental stage. If not, Okio looks like a good backup choice!
@Max No problem! Glad it was helpful. Note that to avoid blocking the UI thread while loading the image, you'll probably want to use
LaunchedEffect
and
<http://Dispatchers.IO|Dispatchers.IO>
, something like:
Copy code
var contactImageBitmap : ImageBitmap? by remember { mutableStateOf(null) }
contactImageBitmap?.let{ Image( it, "Contact Photo" ) }
LaunchedEffect(contactImageRef) {
    contactImageBitmap = 
        withContext(<http://Dispatchers.IO|Dispatchers.IO>) {contactImageRef.readBytes()}
           .map { ImageBitmap.fromByteArray(it) }
           .getOrNull()
}
m

Max

04/14/2024, 10:12 AM
@Matthew Feinberg I got to implement this now, had to modify it the code shared above, but your example helped me still a lot to figure this out. For anybody interested, sharing here my solution: 1. I query the contact in swift an transform the thumbnail
Data
to a `KotlinByteArray`:
Copy code
private func dataToKotlinByteArray(data: Data) -> KotlinByteArray {
    let swiftByteArray = [UInt8](data)
    return swiftByteArray
        .map(Int8.init(bitPattern:))
        .enumerated()
        .reduce(into: KotlinByteArray(size: Int32(swiftByteArray.count))) { result, row in
            result.set(index: Int32(row.offset), value: row.element)
        }
}
2. In CMP i display the image via:
Copy code
Image(
    bitmap = imageLoader.loadImage(it),
    contentDescription = "image",
    modifier = Modifier.size(140.dp).clip(RoundedCornerShape(percent = 50))
)
3. Now comes the tricky part, took me a while to find out: You can only access skia’s ByteArray to Image in iOS source set, otherwise you can while compiling “Could not find skia..“:
Copy code
class IOSImageLoader: ImageLoader {

    override fun loadImage(bytes: ByteArray): ImageBitmap {
        return Image.makeFromEncoded(bytes).toComposeImageBitmap()
    }
}
And … the image is displayed:)
5 Views