夏コミ(C102)は別の記事を書く予定です。
==========================
今年(2023年)のKotlinConf(https://kotlinconf.com/)でCompose Multiplatformが発表されました。これまではCompose Desktopを使うことでAndroidとDesktopでUIコードを共通化することができました。Compose Multiplatformではこれに加えiOSとWebもサポートしています。2023年5月時点ではiOSはAlpha版、WebはExperimental版です。
この章ではAndroidとiOSのUIをCompose Multiplatformで共通化したときに、画像をネットワークから非同期に読み込む方法を紹介します。
ここで紹介する方法は2023年5月時点のものであり、今後APIが変わる可能性が大いにあります。注意してください。
Resources
Compose MultiplatformにはResourceというinterfaceが用意されています。 Resourceにはsuspend関数のreadBytes()が定義されており、戻り値はByteArrayです。
@ExperimentalResourceApi
interface Resource {
suspend fun readBytes(): ByteArray
}
このResourceを実装したLoadImageResourceを用意します。
LoadImageResourceのreadBytes()では指定されたURLからByteArrayを取得する処理を実装します。LoadImageResourceではKtor(https://ktor.io/)を使っています。
@OptIn(ExperimentalResourceApi::class)
class LoadImageResource(private val url: String) : Resource {
override suspend fun readBytes(): ByteArray {
return HttpClient().use {
it.get(url).readBytes()
}
}
}
rememberImageBitmap
Resourceの拡張関数としてrememberImageBitmap()が用意されています。 rememberImageBitmap()はComposable関数で、戻り値はLoadState<ImageBitmap>です。 LaunchedEffectのなかでResourceのreadBytes()を呼び出し、返ってきたByteArrayをtoImageBitmap()でImageBitmapに変換しています。
@ExperimentalResourceApi
@Composable
fun Resource.rememberImageBitmap(): LoadState<ImageBitmap> {
val state: MutableState<LoadState<ImageBitmap>> = remember(this) { mutableStateOf(LoadState.Loading()) }
LaunchedEffect(this) {
state.value = try {
LoadState.Success(readBytes().toImageBitmap())
} catch (e: Exception) {
LoadState.Error(e)
}
}
return state.value
}
LoadStateはsealed classでLoading、Success、Errorの3つのサブクラスがあります。
sealed class LoadState<T> {
class Loading<T> : LoadState<T>()
data class Success<T>(val value: T) : LoadState<T>()
data class Error<T>(val exception: Exception) : LoadState<T>()
}
AsyncImage
画像を読み込んで表示したいURLに対してLoadImageResourceを作り、rememberImageBitmap()の返す LoadStateに応じてComposableを表示します。 LoadState.SuccessのvalueプロパティからImageBitmapを取得し、BitmapPainterでPainterを作ります。 最後にBitmapPainterをImage composableにセットすることで画像が表示されます。
@OptIn(ExperimentalResourceApi::class)
@Composable
fun AsyncImage(
url: String,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
) {
val resource = remember(url) { LoadImageResource(url) }
when (val loadState = resource.rememberImageBitmap()) {
is LoadState.Loading,
is LoadState.Error -> {
Spacer(modifier = modifier.background(Color.LightGray))
}
is LoadState.Success -> {
Image(
painter = BitmapPainter(loadState.value),
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier
)
}
}
}