Slide 1

Slide 1 text

2023/6/15 日本経済新聞社 Androidチーム 小倉 悠里 Jetpack Composeにおける画像ライブラリ ~Coilに🤏だけ詳しくなる~ NIKKEI TECH TALK #8

Slide 2

Slide 2 text

ハッシュタグ #nikkei_tech_talk 自己紹介 名前:小倉 悠里(おぐら ゆり) 経歴: ・2020年7月 ◁⚪◻ボタンとの初めての出会い ・2023年2月 日本経済新聞社に中途入社 好きな食べ物:枝豆 2 小倉なのであんこの写真 @姫路の御座候工場

Slide 3

Slide 3 text

ハッシュタグ #nikkei_tech_talk 日経電子版アプリ ver.6.23.0 ~ 電子版タブに画像バナーが登場🎉 本バナーから特集ページを閲覧したり、 クロスワードパズルに挑戦したりできるように😉 電子版タブのRecyclerViewにJetpack Composeを 一部導入した流れで、本バナーもComposeで実装 その際の検討内容などを共有していきます! 何の話? 3

Slide 4

Slide 4 text

ハッシュタグ #nikkei_tech_talk Jetpack Compose における画像ライブラリは2択 インターネットから画像を読み込みたい 4 ライブラリ Coil Glide github https://github.com/coil-kt/coil https://github.com/bumptech/glide 最新 io.coil-kt:coil-compose:2.4.0 com.github.bumptech.glide:compose: 1.0.0-alpha.3 特徴 高速、軽量、使いやすい、モダン 滑らかなスクロールと速度を 最重視して開発されている 豆知識 💡 Coroutine Image Loader の 頭文字から命名 開発を行っていたBumpをGoogleが買収済み (READMEの免責事項には、 ”This is not an official Google product.” と記載...)

Slide 5

Slide 5 text

ハッシュタグ #nikkei_tech_talk 日経電子版アプリではCoilを選択 ● GlideはJetpack Compose用にまだStable版が出ていない ● Coilが提供するKotlin-firstなAPIは、 Kotlinで記述するComposeと相性が良い ● Now in Android でもCoilを使用 ● 日経電子版のWearアプリにて導入済み(右画像) Coil vs Glide 5

Slide 6

Slide 6 text

ハッシュタグ #nikkei_tech_talk Coil 使用するAPIは3種類 ● AsyncImage ● SubcomposeAsyncImage ● AsyncImagePainter ○ 低レベルAPI ○ AsyncImageもSubcomposeAsyncImageも 内部的にはAsyncImagePainterを使用している 6

Slide 7

Slide 7 text

ハッシュタグ #nikkei_tech_talk Coil 〜AsyncImage〜 ● 非同期的にリクエストと描画を行うAPI ● foundationのImageコンポーザブルとほぼ似た使い勝手 + placeholderやエラー画像などをPainterで指定したり、 読み込み状態の変更を受け取ったりできる 7 AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data("https://example.com/image.jpg") .crossfade(true) .build(), placeholder = painterResource(R.drawable.placeholder), contentDescription = stringResource(R.string.description), contentScale = ContentScale.Crop, modifier = Modifier.clip(CircleShape) )

Slide 8

Slide 8 text

ハッシュタグ #nikkei_tech_talk 【AsyncImageとの違いをざっくり理解】 ● AsyncImage:placeholder / error / fallbackをPainterでセット ● SubcomposeAsyncImage: placeholder / error / fallbackをComposable関数でセット(Slot API) Coil 〜SubcomposeAsyncImage〜 8 SubcomposeAsyncImage( model = "https://example.com/image.jpg", contentDescription = stringResource(R.string.description) ) { val state = painter.state if (state is AsyncImagePainter.State.Loading || state is AsyncImagePainter.State.Error) { CircularProgressIndicator() } else { SubcomposeAsyncImageContent() } }

Slide 9

Slide 9 text

ハッシュタグ #nikkei_tech_talk 【もう少し詳しく理解】 ● SubcomposeLayoutを利用したAsyncImage Coil 〜SubcomposeAsyncImage〜 9 ● サイズの計算はLayoutステップの中で行われている ● SubcomposeLayoutでは、Compositionステップを親のLayoutステップまで 遅らせることで、constraints(制約≒サイズ)の取得を行える https://developer.android.com/jetpack/compose/layouts/basics

Slide 10

Slide 10 text

ハッシュタグ #nikkei_tech_talk Coil 〜Subcompose AsyncImage〜 10 @Composable fun SubcomposeAsyncImage( model: Any?, contentDescription: String?, imageLoader: ImageLoader, modifier: Modifier = Modifier, transform: (State) -> State = DefaultTransform, onState: ((State) -> Unit)? = null, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null, filterQuality: FilterQuality = DefaultFilterQuality, content: @Composable SubcomposeAsyncImageScope.() -> Unit, ) { ... if (sizeResolver !is ConstraintsSizeResolver) { Box(...) { ... } // ※2 } else { BoxWithConstraints( modifier = modifier, contentAlignment = alignment, propagateMinConstraints = true ) { ... } } } SubcomposeAsyncImageの中身を 覗いてみると...👀 BoxWithConstraints(※1) が使われている! ※1:SubcomposeLayoutを利用したComposable ※2:厳密にはSubcomposeAsyncImageでも、 ImageRequestにサイズを明示的に指定した場合には、 constraints取得が不要なためSubcompositionを 使用しない

Slide 11

Slide 11 text

ハッシュタグ #nikkei_tech_talk AsyncImage vs SubcomposeAsyncImage Q. Subcomposition、強そうなのでとりあえず使っておけば良い? A. No 🥺 Subcompositionは拡張性は高いが、パフォーマンスコスト大 ● painterで事足りるケースではAsyncImageを使用 ● Success, Loading, Errorといった描画状態ごとにComposableを用いた表現を行 いたい場合は、SubcomposeAsyncImageを使用💯 ただし、パフォーマンスに注意(リスト中に多用しないなど) 11

Slide 12

Slide 12 text

ハッシュタグ #nikkei_tech_talk 【仕様】 - バナー画像:アスペクト比を保ったまま  (paddingありで)画面横幅いっぱいに表示したい  アスペクト比は、レスポンスで画像URLとともに取得される - placeholder:バナー画像のサイズで外枠をつけたい 日経での導入談 12

Slide 13

Slide 13 text

ハッシュタグ #nikkei_tech_talk placeholder表示にあたり、端末の横幅(親レイアウトのサイズ)が必要 🤔 日経での導入談 13 SubcomposeAsyncImage( modifier = modifier.padding( … ) .aspectRatio((featuredContentsImage.width / featuredContentsImage.height).toFloat()), model = ImageRequest.Builder(LocalContext.current).data(featuredContentsImage.url).build(), contentDescription = featuredContentsBanner.alt, ) { if (painter.state is AsyncImagePainter.State.Success) { SubcomposeAsyncImageContent() } else { Image( modifier = Modifier.border(BorderStroke(1.dp, colorResource(id = R.color.placeholder_border))), painter = painterResource(id = placeHolder), contentDescription = featuredContentsBanner.alt, ) } }

Slide 14

Slide 14 text

ハッシュタグ #nikkei_tech_talk まとめ ● 画像読み込みライブラリ for Compose:Coil vs Glide ● CoilのAsyncImage vs SubcomposeAsyncImage ● SubcomposeAsyncImageの使い所の見極めが大事👀 ● NIKKEIでは、新しいUIにはComposeを積極採用するなど モダンな技術を積極的に取り入れている 14

Slide 15

Slide 15 text

15 ありがとうございました