1. 始める前に
はじめに
これまでの Codelab では、リポジトリ パターンを使用してウェブサービスからデータを取得し、レスポンスを解析して Kotlin オブジェクトに変換する方法を学びました。この Codelab では、その知識に基づいて、ウェブ URL から写真を読み込んで表示します。また、LazyVerticalGrid を作成して概要ページに画像のグリッドを表示する方法もおさらいします。
前提条件
- REST ウェブサービスから JSON を取得する方法と、Retrofit および Gson ライブラリを使用してそのデータを解析し、Kotlin オブジェクトに変換する方法に関する知識
- REST ウェブサービスに関する知識
- Android アーキテクチャ コンポーネント(データレイヤやリポジトリなど)に精通していること
- 依存関係インジェクションに関する知識
ViewModelとViewModelProvider.Factoryに関する知識- アプリのコルーチン実装に関する知識
- リポジトリ パターンの知識
学習内容
- Coil ライブラリを使用してウェブ URL から画像を読み込んで表示する方法。
LazyVerticalGridを使用して画像のグリッドを表示する方法。- 画像をダウンロードして表示する際に発生するエラーの処理方法。
作成するアプリの概要
- Mars Photos アプリを変更して、火星のデータから画像 URL を取得し、Coil を使用してその画像を読み込んで表示します。
- 読み込み中のアニメーションとエラーアイコンをアプリに追加します。
- ステータス処理とエラー処理をアプリに追加します。
必要なもの
- 最新のウェブブラウザ(Chrome の最新バージョンなど)を搭載したパソコン
- REST ウェブサービスを使用した Mars Photos アプリのスターター コード
2. アプリの概要
この Codelab では、前の Codelab で作成した Mars Photos アプリを引き続き操作します。Mars Photos アプリは、ウェブサービスに接続し、Gson を使用して取得した Kotlin オブジェクトの数を取得して表示します。これらの Kotlin オブジェクトには、NASA の火星探査機が撮影した火星表面の実際の写真の URL が含まれています。

この Codelab で作成するバージョンのアプリでは、火星の写真が画像のグリッドで表示されます。これらの画像は、アプリがウェブサービスから取得したデータの一部です。アプリは Coil ライブラリを使用して画像の読み込みと表示を行い、LazyVerticalGrid を使用して画像のグリッド レイアウトを作成します。また、アプリはエラー メッセージを表示して、ネットワーク エラーを適切に処理します。

スターター コードを取得する
まず、スターター コードをダウンロードします。
または、GitHub リポジトリのクローンを作成してコードを入手することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git $ cd basic-android-kotlin-compose-training-mars-photos $ git checkout coil-starter
コードは Mars Photos GitHub リポジトリで確認できます。
3. ダウンロードした画像を表示する
ウェブ URL からの写真を表示するのは簡単に思えますが、適切に処理するにはかなりの作業が必要です。画像をダウンロードして内部的に保存(キャッシュ)し、圧縮形式から Android が使用できる形式にデコードする必要があります。メモリ内キャッシュとストレージベースのキャッシュの両方またはいずれかに画像を保存できます。これらすべてを優先度の低いバックグラウンド スレッドで行いつつ、UI の応答性を維持する必要があります。また、ネットワークと CPU のパフォーマンスを最適化するため、複数の画像を一度に取得してデコードする必要もあります。
幸いなことに、コミュニティで開発された Coil というライブラリを使用して、画像のダウンロード、バッファリング、デコード、キャッシュ保存を行うことができます。Coil を使用しないと、行うべき作業が格段に増えます。
Coil は、基本的に次の 2 つのものを必要とします。
- 読み込んで表示する画像の URL。
- 実際に画像を表示するための
AsyncImageコンポーザブル。
このタスクでは、Coil を使用して、Mars ウェブサービスから取得した単一の画像を表示する方法を学びます。ウェブサービスから返された火星写真のリストに含まれる最初の写真の画像を表示します。次の画像は、変更前と変更後のスクリーンショットを示しています。

Coil の依存関係を追加する
- リポジトリと手動 DI の追加 Codelab から Mars Photos アプリの解答コードを開きます。
- アプリを実行して、火星写真の取得数が表示されることを確認します。
- build.gradle.kts (Module :app) を開きます。
dependenciesセクションで、Coil ライブラリ用に次の行を追加します。
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")
Coil のドキュメント ページでライブラリの最新バージョンを確認し、アップデートします。
- [Sync Now] をクリックし、新しい依存関係でプロジェクトを再ビルドします。
画像の URL を表示する
このステップでは、最初の火星写真の URL を取得して表示します。
getMarsPhotos()メソッドのui/screens/MarsViewModel.ktにあるtryブロック内で、ウェブサービスから取得したデータをlistResultに設定している行を見つけます。
// No need to copy, code is already present
try {
val listResult = marsPhotosRepository.getMarsPhotos()
//...
}
- この行を更新するには、
listResultをresultに変更し、取得した最初の火星写真を新しい変数resultに割り当てます。インデックス0の最初の写真オブジェクトを割り当てます。
try {
val result = marsPhotosRepository.getMarsPhotos()[0]
//...
}
- 次の行で、
MarsUiState.Success()関数呼び出しに渡すパラメータを、次のコード内の文字列に更新します。listResultではなく、新しいプロパティのデータを使用します。写真resultの最初の画像の URL を表示します。
try {
...
MarsUiState.Success("First Mars image URL: ${result.imgSrc}")
}
完全な try ブロックは、次のコードのようになります。
marsUiState = try {
val result = marsPhotosRepository.getMarsPhotos()[0]
MarsUiState.Success(
" First Mars image URL : ${result.imgSrc}"
)
}
- アプリを実行します。
Textコンポーザブルにより、最初の火星写真の URL が表示されます。次のセクションでは、アプリにこの URL の画像を表示させる方法について説明します。

AsyncImage コンポーザブルを追加する
このステップでは、AsyncImage コンポーザブル関数を追加して、1 枚の火星写真を読み込んで表示します。AsyncImage は、画像リクエストを非同期で実行し、結果をレンダリングするコンポーザブルです。
// Example code, no need to copy over
AsyncImage(
model = "https://android.com/sample_image.jpg",
contentDescription = null
)
model 引数には、ImageRequest.data 値または ImageRequest のいずれかを指定できます。上記の例では、ImageRequest.data 値、つまり画像の URL("https://android.com/sample_image.jpg")を割り当てます。次のサンプルコードは、ImageRequest 自体を model に割り当てる方法を示しています。
// Example code, no need to copy over
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)
)
AsyncImage は、標準の Image コンポーザブルと同じ引数をサポートします。また、placeholder / error / fallback ペインタと onLoading / onSuccess / onError コールバックの設定もサポートします。上記のサンプルコードでは、画像を円形の切り抜きとクロスフェードで読み込み、プレースホルダを設定しています。
contentDescription は、この画像が表す内容を示すためにユーザー補助サービスで使用されるテキストを設定します。
AsyncImage コンポーザブルをコードに追加して、最初に取得された火星写真を表示します。
ui/screens/HomeScreen.ktに、MarsPhotoCard()という新しいコンポーズ可能な関数を追加します。これはMarsPhotoとModifierを受け取ります。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
- コンポーズ可能な関数
MarsPhotoCard()内に、次のようにAsyncImage()関数を追加します。
import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.platform.LocalContext
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.build(),
contentDescription = stringResource(R.string.mars_photo),
modifier = Modifier.fillMaxWidth()
)
}
上記のコードでは、画像の URL(photo.imgSrc)を使用して ImageRequest を作成し、model 引数に渡します。contentDescription を使用して、ユーザー補助リーダー用のテキストを設定します。
crossfade(true)をImageRequestに追加して、リクエストが正常に完了したときにクロスフェード アニメーションを有効にします。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.mars_photo),
modifier = Modifier.fillMaxWidth()
)
}
- リクエストが正常に完了したときに
ResultScreenコンポーザブルではなくMarsPhotoCardコンポーザブルを表示するようにHomeScreenコンポーザブルを更新します。型の不一致エラーは次のステップで修正します。
@Composable
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
is MarsUiState.Success -> MarsPhotoCard(photo = marsUiState.photos, modifier = modifier.fillMaxSize())
else -> ErrorScreen(modifier = modifier.fillMaxSize())
}
}
MarsViewModel.ktファイルで、StringではなくMarsPhotoオブジェクトを受け入れるようにMarsUiStateインターフェースを更新します。
sealed interface MarsUiState {
data class Success(val photos: MarsPhoto) : MarsUiState
//...
}
- 最初の火星写真オブジェクトを
MarsUiState.Success()に渡すように、getMarsPhotos()関数を更新します。result変数を削除します。
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos()[0])
}
- アプリを実行し、単一の火星画像が表示されていることを確認します。

- 火星の写真が画面全体に表示されていません。画面上の使用可能なスペースを埋めるには、
HomeScreen.ktおよびAsyncImageでcontentScaleをContentScale.Cropに設定します。
import androidx.compose.ui.layout.ContentScale
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop,
modifier = modifier,
)
}
- アプリを実行し、画像が水平方向と垂直方向の両方で画面全体に表示されることを確認します。

読み込み中の画像とエラーの画像を追加する
画像の読み込み時にプレースホルダ画像を表示して、アプリのユーザー エクスペリエンスを改善できます。画像がない、画像が破損していることなどが原因で読み込みが失敗した場合、エラー画像を表示することもできます。このセクションでは、AsyncImage を使用してエラー画像とプレースホルダ画像の両方を追加します。
res/drawable/ic_broken_image.xmlを開き、右側の [Design] タブまたは [Split] タブをクリックします。エラー画像の場合は、組み込みのアイコン ライブラリにある破損した画像のアイコンを使用します。このベクター型ドローアブルでは、android:tint属性によりアイコンがグレーに色付けされています。

res/drawable/loading_img.xmlを開きます。このドローアブルは、中心点の周りで画像ドローアブル(loading_img.xml)が回転するアニメーションです(アニメーションはプレビューでは確認できません)。

HomeScreen.ktファイルに戻ります。次のコードに示すように、MarsPhotoCardコンポーザブルで、AsyncImage()の呼び出しを更新してerror属性とplaceholder属性を追加します。
import androidx.compose.ui.res.painterResource
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
// ...
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
// ...
)
}
このコードは、読み込み時に使用する読み込み中のプレースホルダ画像(loading_img ドローアブル)を設定します。また、画像の読み込みに失敗した場合に使用する画像(ic_broken_image ドローアブル)も設定します。
完全な MarsPhotoCard コンポーザブルは、次のコードのようになります。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop
)
}
- アプリを実行します。ネットワーク接続の速度によっては、Coil がプロパティ画像をダウンロードして表示する間、一瞬だけ読み込み中の画像が表示されます。しかし、ネットワークを切断しても、破損した画像のアイコンは表示されません。この問題は、Codelab の最後のタスクで修正します。

4. LazyVerticalGrid で画像のグリッドを表示する
以上で、アプリはインターネットから火星写真(最初の MarsPhoto リストアイテム)を読み込むようになりました。この火星写真データの画像 URL を使用して AsyncImage にデータを入力しました。しかし、目標はアプリに画像のグリッドを表示させることです。このタスクでは、LazyVerticalGrid とグリッド レイアウト マネージャーを使用して、画像のグリッドを表示します。
Lazy グリッド
LazyVerticalGrid コンポーザブルと LazyHorizontalGrid コンポーザブルは、アイテムをグリッドに表示するためのサポートを提供しています。Lazy 垂直グリッドは、複数の列にわたって、上下にスクロール可能なコンテナにアイテムを表示し、Lazy 水平グリッドは横軸でそれと同じように動作します。
デザインの観点からすると、グリッド レイアウトは火星写真をアイコンまたは画像として表示するのに最適です。
LazyVerticalGrid の columns パラメータと LazyHorizontalGrid の rows パラメータは、セルを列または行に配置する方法を制御します。次のサンプルコードでは、GridCells.Adaptive を使用して各列の幅を 128.dp 以上に設定し、グリッド形式でアイテムを表示しています。
// Sample code - No need to copy over
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp)
) {
items(photos) { photo ->
PhotoItem(photo)
}
}
}
LazyVerticalGrid を使用するとアイテムの幅を指定でき、グリッドに可能な限り多くの列が収まるようになります。列の数が計算された後、グリッドは残りの幅を列間で均等に分配します。このような適応型のサイズ調整方法は、さまざまな画面サイズでアイテムのセットを表示する場合に特に便利です。
この Codelab では、火星写真を表示するために LazyVerticalGrid コンポーザブルを使用し、GridCells.Adaptive の幅を 150.dp に設定します。
アイテムのキー
ユーザーがグリッド(LazyColumn 内の LazyRow)をスクロールすると、リストアイテムの位置が変更されます。しかし、画面の向きの変更やアイテムの追加または削除によって、ユーザーに表示される行内のスクロール位置が消える可能性があります。アイテムのキーを使用すると、キーに基づいてスクロール位置を維持できます。
キーを指定することで、Compose が並べ替えを正しく処理できるようになります。たとえば、アイテムに記憶された状態が含まれている場合、キーを設定すると、位置が変更されたときに、Compose がこの状態をアイテムと一緒に移動できるようになります。
LazyVerticalGrid を追加する
火星写真のリストを垂直グリッドで表示するコンポーザブルを追加します。
HomeScreen.ktファイルで、PhotosGridScreen()という名前のコンポーズ可能な関数を新たに作成します。この関数は、MarsPhotoのリストとmodifierを引数として受け取ります。
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
}
PhotosGridScreenコンポーザブル内に、次のパラメータでLazyVerticalGridを追加します。
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.dp
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(150.dp),
modifier = modifier.padding(horizontal = 4.dp),
contentPadding = contentPadding,
) {
}
}
- アイテムのリストを追加するには、
LazyVerticalGridラムダ内でitems()関数を呼び出してMarsPhotoのリストとアイテムキーをphoto.idとして渡します。
import androidx.compose.foundation.lazy.grid.items
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
// ...
) {
items(items = photos, key = { photo -> photo.id }) {
}
}
}
- 単一のリストアイテムで表示されるコンテンツを追加するには、
itemsラムダ式を定義します。MarsPhotoCardを呼び出してphotoを渡します。
items(items = photos, key = { photo -> photo.id }) {
photo -> MarsPhotoCard(photo)
}
- リクエストが正常に完了したときに
MarsPhotoCardコンポーザブルではなくPhotosGridScreenコンポーザブルを表示するようにHomeScreenコンポーザブルを更新します。
when (marsUiState) {
// ...
is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
// ...
}
MarsViewModel.ktファイルで、単一のMarsPhotoではなくMarsPhotoオブジェクトのリストを受け入れるように、MarsUiStateインターフェースを更新します。PhotosGridScreenコンポーザブルは、MarsPhotoオブジェクトのリストを受け入れます。
sealed interface MarsUiState {
data class Success(val photos: List<MarsPhoto>) : MarsUiState
//...
}
MarsViewModel.ktファイルで、火星写真オブジェクトのリストをMarsUiState.Success()に渡すようにgetMarsPhotos()関数を更新します。
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
}
- アプリを実行します。

各写真の周囲にパディングはなく、写真ごとにアスペクト比が異なることに注意してください。これらの問題は Card コンポーザブルを追加することで解決できます。
カード コンポーザブルを追加する
HomeScreen.ktファイルのMarsPhotoCardコンポーザブルで、AsyncImageの周囲に8.dpのエレベーションを持つCardを追加します。modifier引数をCardコンポーザブルに割り当てます。
import androidx.compose.material.Card
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
}
}
- アスペクト比を修正するには、
PhotosGridScreen()でMarsPhotoCard()の修飾子を更新します。
@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
LazyVerticalGrid(
//...
) {
items(items = photos, key = { photo -> photo.id }) { photo ->
MarsPhotoCard(
photo,
modifier = modifier
.padding(4.dp)
.fillMaxWidth()
.aspectRatio(1.5f)
)
}
}
}
PhotosGridScreen()をプレビューするように結果画面のプレビューを更新します。空の画像 URL のモックデータ
@Preview(showBackground = true)
@Composable
fun PhotosGridScreenPreview() {
MarsPhotosTheme {
val mockData = List(10) { MarsPhoto("$it", "") }
PhotosGridScreen(mockData)
}
}
モックデータには空の URL があるため、写真グリッドのプレビューに画像の読み込みが表示されます。

- アプリを実行します。

- アプリの実行中に機内モードをオンにします。
- エミュレータで画像をスクロールします。まだ読み込まれていない画像が、破損した画像のアイコンとして表示されます。これは、ネットワーク エラーが発生したときや画像を取得できないときに表示するために Coil 画像ライブラリに渡した画像ドローアブルです。

お疲れさまでした。エミュレータまたはデバイスで機内モードをオンにして、ネットワーク接続エラーをシミュレートしました。
5. 再試行アクションを追加する
このセクションでは、再試行アクション ボタンを追加し、ボタンがクリックされたときに写真を取得します。

- エラー画面にボタンを追加します。
HomeScreen.ktファイルで、retryActionラムダ パラメータとボタンを含めるようにErrorScreen()コンポーザブルを更新します。
@Composable
fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
Column(
// ...
) {
Image(
// ...
)
Text(//...)
Button(onClick = retryAction) {
Text(stringResource(R.string.retry))
}
}
}
プレビューの確認

- 再試行ラムダを渡すように
HomeScreen()コンポーザブルを更新します。
@Composable
fun HomeScreen(
marsUiState: MarsUiState, retryAction: () -> Unit, modifier: Modifier = Modifier
) {
when (marsUiState) {
//...
is MarsUiState.Error -> ErrorScreen(retryAction, modifier = modifier.fillMaxSize())
}
}
ui/theme/MarsPhotosApp.ktファイルのHomeScreen()関数呼び出しを更新し、retryActionラムダ パラメータをmarsViewModel::getMarsPhotosに設定します。これにより、サーバーから火星の写真が取得されます。
HomeScreen(
marsUiState = marsViewModel.marsUiState,
retryAction = marsViewModel::getMarsPhotos
)
6. ViewModel テストを更新する
MarsUiState と MarsViewModel が、1 枚の写真ではなく、写真のリストに対応するようになりました。現在の状態では、MarsViewModelTest は MarsUiState.Success データクラスが文字列プロパティを含むことを想定しています。そのため、テストはコンパイルされません。marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() テストを更新して、MarsViewModel.marsUiState が写真のリストを含む Success の状態と等しいことを確認する必要があります。
rules/MarsViewModelTest.ktファイルを開きます。marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()テストでは、assertEquals()関数呼び出しを変更して、Success状態(架空の写真リストを photos パラメータに渡す)をmarsViewModel.marsUiStateと比較します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest {
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
assertEquals(
MarsUiState.Success(FakeDataSource.photosList),
marsViewModel.marsUiState
)
}
これでテストのコンパイル、実行が完了し、合格できました。
7. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
この Codelab の解答コードを確認する場合は、GitHub で表示します。
8. まとめ
おつかれさまです。これで Codelab は完了し、Mars Photos アプリが完成しました。アプリを使って家族や友人に実際の火星の写真を見せてあげましょう。
作成したら、#AndroidBasics を付けて、ソーシャル メディアで共有しましょう。
9. 関連リンク
Android デベロッパー ドキュメント:
- リストとグリッド | Jetpack Compose | Android デベロッパー
- Lazy グリッド | Jetpack Compose | Android デベロッパー
- ViewModel の概要
その他:
