夏コミ(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
- }
- @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
- }
- 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
- )
- }
- }
- }