概要
前回の Codelab では、ウェブサービスからデータを取得し、レスポンスを解析して Kotlin オブジェクトに変換する方法を学習しました。この Codelab では、その知識に基づいて、ウェブ URL から写真を読み込んで表示します。また、RecyclerView を作成して概要ページに画像のグリッドを表示する方法もおさらいします。
前提条件
- フラグメントの作成方法と使用方法。
- REST ウェブサービスから JSON を取得する方法と、Retrofit ライブラリおよび Moshi ライブラリを使用してそのデータを解析し、Kotlin オブジェクトに変換する方法。
RecyclerViewを使用してグリッド レイアウトを作成する方法。Adapter、ViewHolder、DiffUtilの機能。
学習内容
- Coil ライブラリを使用してウェブ URL から画像を読み込んで表示する方法。
RecyclerViewとグリッド アダプターを使用して画像のグリッドを表示する方法。- 画像をダウンロードして表示する際に発生するエラーの処理方法。
作成するアプリの概要
- MarsPhotos アプリを変更して、Mars データから画像 URL を取得し、Coil を使用してその画像を読み込んで表示します。
- 読み込み中のアニメーションとエラーアイコンをアプリに追加します。
RecyclerViewを使用して火星画像のグリッドを表示します。- ステータス処理とエラー処理を
RecyclerViewに追加します。
必要なもの
- 最新のウェブブラウザ(Chrome の最新バージョンなど)を搭載したパソコン
- パソコンでのインターネット アクセス
この Codelab では、前回の Codelab で作成した MarsPhotos というアプリを引き続き使用します。MarsPhotos アプリは、ウェブサービスに接続し、Retrofit を使用して取得した Kotlin オブジェクトの数を取得して表示します。これらの Kotlin オブジェクトには、NASA の火星探査機が撮影した火星表面の実際の写真の URL が含まれています。
この Codelab で作成するバージョンのアプリは概要ページにデータを書き込みます。概要ページでは火星の写真を画像グリッドで表示します。これらの画像は、アプリが Mars ウェブサービスから取得したデータの一部です。アプリは Coil ライブラリを使用して画像の読み込みと表示を行い、RecyclerView を使用して画像のグリッド レイアウトを作成します。また、アプリはネットワーク エラーを適切に処理します。

ウェブ URL からの写真を表示するのは簡単に思えますが、適切に処理するにはかなりの作業が必要です。画像をダウンロードして内部的に保存し、圧縮形式から Android が使用できる形式にデコードする必要があります。メモリ内キャッシュとストレージベースのキャッシュの両方またはいずれかに画像を保存する必要があります。これらすべてを優先度の低いバックグラウンド スレッドで行いつつ、UI の応答性を維持する必要があります。また、ネットワークと CPU のパフォーマンスを最適化するため、複数の画像を一度に取得してデコードする必要もあります。
幸いなことに、コミュニティで開発された Coil というライブラリを使用して、画像のダウンロード、バッファリング、デコード、キャッシュ保存を行うことができます。Coil を使用しないと、行うべき作業が格段に増えます。
Coil は、基本的に次の 2 つのものを必要とします。
- 読み込んで表示する画像の URL。
- 実際に画像を表示するための
ImageViewオブジェクト。
このタスクでは、Coil を使用して、Mars ウェブサービスから取得した単一の画像を表示する方法を学びます。ウェブサービスから返された火星写真のリストに含まれる最初の写真の画像を表示します。画像の表示前と表示後のスクリーンショットを次に示します。
|
|
Coil の依存関係を追加する
- 前回の Codelab で作成した MarsPhotos アプリの解答コードを開きます。
- アプリを実行して動作を確認します(取得した火星の写真の合計数が表示されます)。
- build.gradle(モジュール: app)を開きます。
dependenciesセクションで、Coil ライブラリ用に次の行を追加します。
// Coil
implementation "io.coil-kt:coil:1.1.1"
Coil のドキュメント ページでライブラリの最新バージョンを確認し、アップデートします。
- Coil ライブラリは
mavenCentral()リポジトリでホストされており、そこから利用できます。build.gradle(プロジェクト: MarsPhotos)で、一番上のrepositoriesブロックにmavenCentral()を追加します。
repositories {
google()
jcenter()
mavenCentral()
}
- [Sync Now] をクリックし、新しい依存関係でプロジェクトを再ビルドします。
ViewModel を更新する
このステップでは、受け取った Kotlin オブジェクト(MarsPhoto)を格納する LiveData プロパティを OverviewViewModel クラスに追加します。
overview/OverviewViewModel.ktを開きます。_statusプロパティの宣言のすぐ下に、単一のMarsPhotoオブジェクトを格納できるMutableLiveData型の新しい可変プロパティを_photosという名前で追加します。
private val _photos = MutableLiveData<MarsPhoto>()
リクエストされたら、com.example.android.marsphotos.network.MarsPhoto をインポートします。
_photosの宣言のすぐ下に、LiveData<MarsPhoto>型の公開バッキング フィールドをphotosという名前で追加します。
val photos: LiveData<MarsPhoto> = _photos
getMarsPhotos()メソッドのtry{}ブロック内で、ウェブサービスから取得したデータをlistResult.に設定している行を見つけます。
try {
val listResult = MarsApi.retrofitService.getPhotos()
...
}
- 最初に取得した火星写真を新しい変数
_photosに割り当てます。listResultを_photos.valueに変更します。インデックス0の最初の写真 URL を割り当てます。エラーがスローされますが、これは後で修正します。
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
...
}
- 次の行で、
status.valueを以下のように更新します。listResultではなく、新しいプロパティのデータを使用します。写真リストの最初の画像 URL を表示します。.
try {
...
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- 完全な
try{}ブロックは次のようになります。
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- アプリを実行します。
TextViewにより、最初の火星写真の URL が表示されます。ここまでの作業では、その URL 用にViewModelとLiveDataを設定しただけです。

バインディング アダプターを使用する
バインディング アダプターは、ビューのカスタム プロパティ用のカスタム セッターを作成するために使用される、アノテーション付きのメソッドです。
通常は、android:text="Sample Text" というコードを使用して XML で属性を設定する場合に使用します。Android システムは、setText(String: text) メソッドで設定された、text 属性と同じ名前のセッターを自動的に探します。setText(String: text) メソッドは、Android フレームワークが提供するいくつかのビューのためのセッター メソッドです。バインディング アダプターを使用すると、同様の動作をカスタマイズできます。つまり、データ バインディング ライブラリによって呼び出されるカスタム属性とカスタム ロジックを指定できます。
例:
単に画像ビューでセッターを呼び出す以上に複雑な処理を行うには、ドローアブル画像を設定します。インターネットから UI スレッド(メインスレッド)外の画像を読み込むことを検討してください。最初に、画像を ImageView に割り当てるカスタム属性を選択します。次の例では、imageUrl がそれに該当します。
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{product.imageUrl}"/>
それ以上コードを追加しなければ、システムは ImageView で setImageUrl(String) メソッドを検索し、見つけられずにエラーをスローします。これは、フレームワークが提供していないカスタム属性だからです。app:imageUrl 属性を実装して ImageView に設定する方法を考案する必要があります。そのためには、バインディング アダプター(アノテーション付きメソッド)を使用します。
バインディング アダプターの例:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
// Load the image in the background using Coil.
}
}
}
@BindingAdapter アノテーションは、パラメータとして属性名を受け取ります。
bindImage メソッドの 1 つ目のメソッド パラメータはターゲット ビューの型で、2 つ目のメソッド パラメータは属性に設定される値です。
このメソッド内で、Coil ライブラリは UI スレッド外の画像を読み込み、それを ImageView に設定します。
バインディング アダプターを作成し、Coil を使用する
com.example.android.marsphotosパッケージに、BindingAdaptersという名前の Kotlin ファイルを作成します。このファイルは、アプリ全体で使用するバインディング アダプターを保持します。

BindingAdapters.kt内に、パラメータとしてImageViewとStringを受け取るbindImage()関数を作成します。
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
リクエストされたら、android.widget.ImageView をインポートします。
- 関数に
@BindingAdapterアノテーションを付けます。@BindingAdapterアノテーションは、ビューアイテムにimageUrl属性がある場合にこのバインディング アダプターを実行するようデータ バインディングに指示します。
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
リクエストされたら、androidx.databinding.BindingAdapter をインポートします。
let スコープ関数
let は Kotlin のスコープ関数のひとつであり、オブジェクトのコンテキスト内でコードブロックを実行することを可能にします。Kotlin には 5 つのスコープ関数があります。詳しくは、ドキュメントをご覧ください。
使用方法:
let を使用して、コールチェーンの結果に対して 1 つ以上の関数を呼び出します。
let 関数と safe call 演算子(?.)を併用して、null セーフな演算をオブジェクトに実行します。この場合、オブジェクトが null でない場合にのみ、let コードブロックが実行されます。
bindImage()関数内で、safe call 演算子を使用してlet{}ブロックをimageURL引数に追加します。
imgUrl?.let {
}
let{}ブロック内に次の行を追加し、toUri()メソッドを使用して URL 文字列をUriオブジェクトに変換します。HTTPS スキームを使用するには、buildUpon.scheme("https")をtoUriビルダーの末尾に追加します。build()を呼び出してオブジェクトを作成します。
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
リクエストされたら、androidx.core.net.toUri をインポートします。
let{}ブロック内のimgUri宣言の後で、Coil からのload(){}を使用して、imgUriオブジェクトから画像をimgViewに読み込みます。
imgView.load(imgUri) {
}
リクエストされたら、coil.load をインポートします。
- 完全なメソッドは次のようになります。
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri)
}
}
レイアウトとフラグメントを更新する
前のセクションでは、Coil 画像ライブラリを使用して画像を読み込みました。次のステップとして、画面に画像を表示するため、新しい属性で ImageView を更新し、単一の画像を表示します。
Codelab の後半では、RecyclerView 内の各グリッド アイテムのレイアウト リソース ファイルとして、res/layout/grid_view_item.xml を使用します。このタスクでは、このファイルを一時的に使用して、前のタスクで取得した画像 URL で画像を表示します。fragment_overview.xml の代わりに、このレイアウト ファイルを一時的に使用します。
res/layout/grid_view_item.xmlを開きます。<ImageView>要素の上にデータ バインディング用の<data>要素を追加して、OverviewViewModelクラスにバインドします。
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
- 新しい画像読み込みバインディング アダプターを使用するため、
ImageView要素にapp:imageUrl属性を追加します。photosには、サーバーから取得されたリストMarsPhotosが含まれていることを思い出してください。最初のエントリ URL をimageUrl属性に割り当てます。
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.photos.imgSrcUrl}"
... />
overview/OverviewFragment.ktを開きます。onCreateView()メソッドで、FragmentOverviewBindingクラスをインフレートする行をコメントアウトし、バインディング変数に割り当てます。この行を削除したことにより、エラーが表示されます。これは一時的なものであり、後で修正します。
//val binding = FragmentOverviewBinding.inflate(inflater)
fragment_overview.xml.の代わりにgrid_view_item.xmlを使用します。次の行を追加して、GridViewItemBindingクラスをインフレートします。
val binding = GridViewItemBinding.inflate(inflater)
リクエストされた場合は、com.example.android.marsphotos. databinding.GridViewItemBinding をインポートします。
- アプリを実行します。単一の火星画像が表示されます。

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

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

BindingAdapters.ktファイルに戻ります。bindImage()メソッドで、imgView.load(imgUri)の呼び出しを更新して、以下のように末尾のラムダを追加します。このコードは、読み込み時に使用する読み込み中のプレースホルダ画像(loading_animationドローアブル)を設定します。また、画像の読み込みが失敗した場合に使用する画像(broken_imageドローアブル)も設定します。
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
- 完全な
bindImage()メソッドは次のようになります。
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
}
}
- アプリを実行します。ネットワーク接続の速度によっては、Glide がプロパティ画像をダウンロードして表示する間、一瞬だけ読み込み中の画像が表示されます。しかし、ネットワークを切断しても、破損した画像のアイコンは表示されません。この問題は、Codelab の最後のタスクで修正します。

overview/OverviewFragment.ktに加えた一時的な変更を元に戻します。メソッドonCreateview()で、FragmentOverviewBindingをインフレートする行のコメント化を解除します。GridViewIteMBindingをインフレートする行を削除するか、コメントアウトします。
val binding = FragmentOverviewBinding.inflate(inflater)
// val binding = GridViewItemBinding.inflate(inflater)
以上で、アプリはインターネットから火星の写真を読み込むようになりました。最初の MarsPhoto リストアイテムからのデータを使用して ViewModel に LiveData プロパティを作成し、その火星写真データからの画像 URL を使用して ImageView にデータを入力しました。しかし、アプリの目標は画像のグリッドを表示することです。そこで、このタスクでは、グリッド レイアウト マネージャーで RecyclerView を使用して、画像のグリッドを表示します。
ビューモデルを更新する
前のタスクでは、OverviewViewModel に _photos という名前の LiveData オブジェクトを追加しました。このオブジェクトは、ウェブサービスからのレスポンス リストに含まれる最初のオブジェクトである MarsPhoto オブジェクトを保持します。このステップでは、この LiveData を変更して、MarsPhoto オブジェクトのリスト全体を保持するようにします。
overview/OverviewViewModel.ktを開きます。_photosの型をMarsPhotoオブジェクトのリストに変更します。
private val _photos = MutableLiveData<List<MarsPhoto>>()
- 同様に、バッキング プロパティ
photosの型をList<MarsPhoto>型に置き換えます。
val photos: LiveData<List<MarsPhoto>> = _photos
- 下にスクロールして、
getMarsPhotos()メソッド内のtry {}ブロックを表示します。MarsApi.retrofitService.getPhotos()は、
MarsPhoto オブジェクトのリストを返します。これを _photos.value に割り当てることができます。
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
try/catchブロック全体は次のようになります。
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
グリッド レイアウト
RecyclerView の GridLayoutManager は、以下に示すように、スクロール可能なグリッドとしてデータを配置します。

デザインの観点から見ると、グリッド レイアウトはアイコンまたは画像として表現できるリスト(たとえば、火星写真を閲覧するアプリ内のリスト)に最適です。
グリッド レイアウトがアイテムを配置する仕組み
グリッド レイアウトは、行と列からなるグリッドにアイテムを配置します。縦方向のスクロールを想定した場合、デフォルトでは、各行の各アイテムは 1 つの「スパン」を占有します。1 つのアイテムは複数のスパンを占有できます。下記の例では、1 つのスパンは 3 つある列の 1 つの列の幅に相当します。
下記の 2 つの例では、各行は 3 つのスパンで構成されています。デフォルトでは、GridLayoutManager は、指定したスパン数に達するまで、各アイテムを 1 つのスパンに配置します。指定したスパン数に達すると、次の行に折り返します。
|
|
RecyclerView を追加する
このステップでは、アプリのレイアウトを変更して、単一の画像ビューを使用する代わりに、グリッド レイアウトを持つ RecyclerView を使用するようにします。
layout/gridview_item.xmlを開きます。viewModelデータ変数を削除します。<data>タグ内に、MarsPhoto型のphoto変数(以下を参照)を追加します。
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
<ImageView>内でapp:imageUrl属性を変更して、MarsPhotoオブジェクトに含まれる画像 URL を参照するようにします。上記の変更により、前のタスクで行った一時的な変更が取り消されます。
app:imageUrl="@{photo.imgSrcUrl}"
layout/fragment_overview.xmlを開きます。<TextView>要素全体を削除します。- 代わりに以下の
<RecyclerView>要素を追加します。ID をphotos_gridに設定します。widthおよびheight属性を0dpに設定して、親ConstraintLayout全体を埋めるようにします。グリッド レイアウトを使用するため、layoutManager属性をandroidx.recyclerview.widget.GridLayoutManagerに設定します。spanCountを2に設定して、2 つの列を持つようにします。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2" />
- 上記のコードをデザインビューでプレビューするため、
tools:itemCountを使用して、レイアウトに表示するアイテムの数を16に設定します。itemCount属性は、[Preview] ウィンドウで Layout Editor によりレンダリングされるアイテムの数を規定します。tools:listitemを使用して、リストアイテムのレイアウトをgrid_view_itemに設定します。
<androidx.recyclerview.widget.RecyclerView
...
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
- デザインビューに切り替えると、次のスクリーンショットのようなプレビューが表示されます。これは火星の写真ではありませんが、RecyclerView のグリッド レイアウトがどのように表示されるかを示しています。プレビューでは、
recyclerviewのグリッド アイテムごとに、パディングとgrid_view_itemレイアウトが使用されています。

- マテリアル デザイン ガイドラインに従うには、リストの上下左右に
8dp、各アイテム間に4dpのスペースが必要です。これを実現するには、fragment_overview.xmlレイアウトとgridview_item.xmlレイアウトのパディングを組み合わせて使用します。

layout/gridview_item.xmlを開きます。お気づきのとおり、padding属性では、各アイテム間とコンテンツ全体の周囲に2dpのパディングがすでに設定されています。これにより、各アイテム間に4dp、コンテンツ全体の周囲に2dpのスペースが空いています。つまり、デザイン ガイドラインに従うには、コンテンツ全体の周囲に6dpのパディングを追加する必要があります。layout/fragment_overview.xmlに戻ります。RecyclerViewに6dpのパディングを追加します。これにより、ガイドラインが示すとおり、コンテンツ全体の周囲に8dp、コンテンツ内のアイテム間に4dpのスペースが空けられます。
<androidx.recyclerview.widget.RecyclerView
...
android:padding="6dp"
... />
- 完全な
<RecyclerView>要素は次のようになります。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
写真グリッド アダプターを追加する
以上で、fragment_overview レイアウトに、グリッド レイアウトを持つ RecyclerView が含まれるようになりました。このステップでは、RecyclerView アダプターを使用して、ウェブサーバーから取得したデータを RecyclerView にバインドします。
ListAdapter(リフレッシャー)
ListAdapter は RecyclerView.Adapter クラスのサブクラスで、RecyclerView にリストデータを表示するために使用します。バックグラウンド スレッドでリスト間の差分計算も行います。
このアプリでは、ListAdapter. で DiffUtil 実装を使用します。DiffUtil を使用するメリットは、RecyclerView でアイテムが追加、削除、変更されるたびにリスト全体が更新されるのを避けられることです。変更されたアイテムのみが更新されます。
アプリに ListAdapter を追加します。
overviewパッケージに、PhotoGridAdapter.ktという名前の Kotlin クラスを作成します。- 下記のコンストラクタ パラメータを使って、
ListAdapterからPhotoGridAdapterクラスを拡張します。PhotoGridAdapterクラスはListAdapterを拡張します。このクラスのコンストラクタには、リストアイテムの型、ビューホルダー、DiffUtil.ItemCallback実装が必要です。
class PhotoGridAdapter : ListAdapter<MarsPhoto,
PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}
リクエストされた場合は、androidx.recyclerview.widget.ListAdapter クラスと com.example.android.marsphoto.network.MarsPhoto クラスをインポートします。次のステップでは、このコンストラクタに欠けている(それによってエラーを引き起こしている)別の機能を実装します。
- 上記のエラーを解決するため、このステップで必要なメソッドを追加し、このタスクの後半でそれらを実装します。
PhotoGridAdapterクラスをクリックし、赤い電球アイコンをクリックして、プルダウン メニューから [Implement members] を選択します。表示されたポップアップで、ListAdapterのメソッドであるonCreateViewHolder()とonBindViewHolder()を選択します。依然としてエラーが表示されますが、それらはこのタスクの最後で修正します。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPhotoViewHolder {
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPhotoViewHolder, position: Int) {
TODO("Not yet implemented")
}
onCreateViewHolder メソッドと onBindViewHolder メソッドを実装するには、次のステップで追加する MarsPhotoViewHolder が必要です。
PhotoGridAdapter内で、RecyclerView.ViewHolderを拡張するMarsPhotoViewHolderの内部クラス定義を追加します。MarsPhotoをレイアウトにバインドするためにGridViewItemBinding変数が必要なので、変数をMarsPhotoViewHolderに渡します。基本クラスViewHolderではコンストラクタ内にビューが必要なので、バインディング ルートビューを渡します。
class MarsPhotoViewHolder(private var binding:
GridViewItemBinding):
RecyclerView.ViewHolder(binding.root) {
}
リクエストされた場合は、androidx.recyclerview.widget.RecyclerView と com.example.android.marsrealestate.databinding.GridViewItemBinding をインポートします。
MarsPhotoViewHolder内で、MarsPhotoオブジェクトを引数として受け取り、binding.propertyをそのオブジェクトに設定するbind()メソッドを作成します。プロパティを設定した後、executePendingBindings()を呼び出します。これにより、更新が直ちに実行されます。
fun bind(MarsPhoto: MarsPhoto) {
binding.photo = MarsPhoto
binding.executePendingBindings()
}
- 引き続き
onCreateViewHolder()のPhotoGridAdapterクラス内で、TODO を削除して下記の行を追加します。onCreateViewHolder()メソッドは新しいMarsPhotoViewHolderを返す必要があります。このビューホルダーは、GridViewItemBindingをインフレートし、親ViewGroupコンテキストからのLayoutInflaterを使用することにより、作成されます。
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
リクエストされた場合は、android.view.LayoutInflater をインポートします。
onBindViewHolder()メソッドで、TODO を削除して下記の行を追加します。ここでgetItem()を呼び出し、現在のRecyclerViewの位置に関連付けられているMarsPhotoオブジェクトを取得して、そのプロパティをMarsPhotoViewHolderのbind()メソッドに渡します。
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
- 以下に示すように、
PhotoGridAdapter内にDiffCallbackのコンパニオン オブジェクト定義を追加します。
DiffCallbackオブジェクトは、比較したいオブジェクト(MarsPhoto)の汎用の型を使ってDiffUtil.ItemCallbackを拡張します。この実装内で、2 つの火星写真オブジェクトを比較します。
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}
リクエストされたら、androidx.recyclerview.widget.DiffUtil をインポートします。
- 赤い電球アイコンをクリックして、
DiffCallbackオブジェクトのコンパレータ メソッド(areItemsTheSame()とareContentsTheSame())を実装します。
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented")
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented") }
areItemsTheSame()メソッド内のTODOを削除します。このメソッドは、2 つのオブジェクトが同じアイテムを表しているかどうかを判定するために、DiffUtilによって呼び出されます。DiffUtilは、このメソッドを使用して、新しいMarsPhotoオブジェクトが古いMarsPhotoオブジェクトと同じかどうかを確認します。すべてのアイテム(MarsPhotoオブジェクト)の ID は一意です。oldItemとnewItemの ID を比較して結果を返します。
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
areContentsTheSame()内のTODOを削除します。このメソッドは、2 つのアイテムが同じデータを持つかどうかをチェックする際に、DiffUtilによって呼び出されます。MarsPhoto の重要なデータは画像 URL です。oldItemとnewItemの URL を比較して結果を返します。
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
エラーなしでアプリをコンパイルして実行できることを確認します。ただし、エミュレータでは空白の画面が表示されます。以上で RecyclerView の準備が完了しましたが、まだデータが渡されていません。次のステップでは、この機能を実装します。
バインディング アダプターを追加して要素を接続する
このステップでは、BindingAdapter を使用して、MarsPhoto オブジェクトのリストで PhotoGridAdapter を初期化します。BindingAdapter を使用して RecyclerView のデータを設定すると、LiveData に MarsPhoto オブジェクトのリストがあるかどうかがデータ バインディングによって自動的に監視されるようになります。MarsPhoto リストが変更されると、バインディング アダプターが自動的に呼び出されます。
BindingAdapters.ktを開きます。- ファイルの末尾に、
RecyclerViewとMarsPhotoオブジェクトのリストを引数として受け取るbindRecyclerView()メソッドを追加します。そのメソッドに、listData属性を含む@BindingAdapterアノテーションを付けます。
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
}
リクエストされた場合は、androidx.recyclerview.widget.RecyclerView と com.example.android.marsphotos.network.MarsPhoto をインポートします。
bindRecyclerView()関数内で、recyclerView.adapterをPhotoGridAdapterにキャストして、新しいvalプロパティadapter.に割り当てます。
val adapter = recyclerView.adapter as PhotoGridAdapter
bindRecyclerView()関数の最後で、火星写真リストのデータを指定してadapter.submitList()を呼び出します。これにより、新しいリストが利用可能であることがRecyclerViewに通知されます。
adapter.submitList(data)
リクエストされた場合は、com.example.android.marsrealestate.overview.PhotoGridAdapter をインポートします。
- 完全な
bindRecyclerViewバインディング アダプターは次のようになります。
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
- すべての要素を結合するため、
res/layout/fragment_overview.xmlを開きます。RecyclerView要素にapp:listData属性を追加し、データ バインディングを使用してそれをviewmodel.photosに設定します。これは、前のタスクでImageViewについて行った作業と似ています。
app:listData="@{viewModel.photos}"
overview/OverviewFragment.ktを開きます。onCreateView()のreturnステートメントの直前で、binding.photosGridのRecyclerViewアダプターを新しいPhotoGridAdapterオブジェクトに初期化します。
binding.photosGrid.adapter = PhotoGridAdapter()
- アプリを実行します。スクロール可能な火星画像のグリッドが表示されます。スクロールすると新しい画像が表示されますが、少し違和感があるはずです。スクロール中も
RecyclerViewの上下にパディングが残っているため、アクションバーの下でリストがスクロールされているように見えません。

- これを修正するには、android:clipToPadding 属性を使用して、内側のコンテンツをパディングに合わせてクリッピングしないよう
RecyclerViewに指示する必要があります。そうすれば、パディング領域にスクロール ビューが描画されます。layout/fragment_overview.xmlに戻ります。RecyclerViewにandroid:clipToPadding属性を追加してfalseに設定します。
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
- アプリを実行します。お気づきのとおり、目的の画像自体が表示される前に、読み込みが進行中であることを示すアイコンが表示されます。これは、Coil 画像ライブラリに渡した読み込み中のプレースホルダ画像です。

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

おつかれさまです。作業はあと少しで完了します。次の最後のタスクでは、エラー処理をアプリに追加して、ユーザー エクスペリエンスをさらに改善します。
MarsPhotos アプリは、画像を取得できない場合、破損した画像のアイコンを表示します。しかし、ネットワーク接続がない場合は、空白の画面を表示します。次のステップでは、空白の画面を確認します。
- デバイスまたはエミュレータで機内モードをオンにします。Android Studio からアプリを実行します。空白の画面が表示されます。

これはユーザー エクスペリエンスとしては不適切です。このタスクでは、基本的なエラー処理を追加して、何が起こっているかをユーザーが理解できるようにします。インターネットが利用できない場合、アプリは接続エラーアイコンを表示します。一方、MarsPhoto リストの取得中は、読み込み中のアニメーションを表示します。
ViewModel にステータスを追加する
このタスクでは、ウェブ リクエストのステータスを表すプロパティを OverviewViewModel に作成します。考慮する必要があるステータスは、読み込み中、成功、失敗の 3 つです。「読み込み中」ステータスは、データを待機している状態を示します。「成功」ステータスは、ウェブサービスからデータが正常に取得されたことを示します。「失敗」ステータスは、ネットワーク エラーまたは接続エラーが発生したことを示します。
Kotlin の enum クラス
アプリ内で上記の 3 つのステータスを表現するには、enum を使用します。enum は列挙型の略称で、コレクションに含まれるすべてのアイテムの順序付きリストを意味します。個々の enum 定数は、enum クラスのオブジェクトです。
Kotlin の enum は、定数のセットを保持できるデータ型です。以下に示すように、クラス定義の前にキーワード enum を追加することにより定義します。enum 定数はカンマで区切ります。
定義:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
使用方法:
var direction = Direction.NORTH;
上記のように、enum オブジェクトは、クラス名の後にドット(.)演算子と定数名を付けることにより参照できます。
ステータス値を含む enum クラス定義を ViewModel に追加します。
overview/OverviewViewModel.ktを開きます。ファイルの先頭部分(インポートの後、クラス定義の前)に、すべての使用可能なステータスを表すenumを追加します。
enum class MarsApiStatus { LOADING, ERROR, DONE }
- スクロールして
_statusとstatusプロパティの定義を表示し、型をStringからMarsApiStatus. MarsApiStatusに変更します。これは、前のステップで定義した enum クラスです。
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus> = _status
getMarsPhotos()メソッドで、"Success: ..."文字列をMarsApiStatus.DONE状態に変更し、"Failure..."文字列をMarsApiStatus.ERRORに変更します。
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception)
_status.value = MarsApiStatus.ERROR
}
try {}ブロックの上で、ステータスをMarsApiStatus.LOADINGに設定します。これは、コルーチンを実行してデータを待機しているときの初期ステータスです。完全なviewModelScope.launch{}ブロックは次のようになります。
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
}
}
catch {}ブロックのエラー状態の後で、_photosを空のリストに設定します。これにより、RecyclerView がクリアされます。
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
- 完全な
getMarsPhotos()メソッドは次のようになります。
private fun getMarsPhotos() {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
}
}
以上で、ステータスに enum 状態を定義し、コルーチンの開始時に「読み込み中」状態を設定しました。アプリがウェブサーバーからのデータの取得を完了したときは完了状態を設定し、例外が発生したときはエラー状態を設定しました。次のタスクでは、バインディング アダプターを使用して、対応するアイコンを表示します。
ステータス ImageView のバインディング アダプターを追加する
enum 状態のセットを使用して、OverviewViewModel に MarsApiStatus を設定しました。このステップでは、アプリでそれが表示されるようにします。ImageView のバインディング アダプターを使用して、読み込み中とエラーの状態を表すアイコンを表示します。アプリが読み込み中またはエラーの状態にあるときは、ImageView を表示する必要があります。アプリが読み込みを完了したときは、ImageView を非表示にする必要があります。
BindingAdapters.ktを開いて、ファイルの末尾にスクロールし、別のアダプターを追加します。ImageViewとMarsApiStatus値を引数として受け取るbindStatus()という名前の新しいバインディング アダプターを追加します。メソッドに@BindingAdapterアノテーションを付け、パラメータとしてカスタム属性marsApiStatusを渡します。
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
リクエストされた場合は、com.example.android.marsrealestate.overview.MarsApiStatus をインポートします。
- ステータスに応じて処理を切り替えるため、
bindStatus()メソッド内にwhen {}ブロックを追加します。
when (status) {
}
when {}内に、「読み込み中」状態(MarsApiStatus.LOADING)の処理を追加します。この状態の場合は、ImageViewを「表示」に設定し、読み込み中のアニメーションを割り当てます。これは、前のタスクで Coil について使用したのと同じアニメーション ドローアブルです。
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
リクエストされた場合は、android.view.View をインポートします。
- エラー状態(
MarsApiStatus.ERROR)の処理を追加します。LOADING状態の処理と同様に、ステータスImageViewを「表示」に設定し、接続エラー ドローアブルを使用します。
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
- 完了状態(
MarsApiStatus.DONE)の処理を追加します。この場合は成功レスポンスが返されるので、ステータスImageViewの表示をView.GONEに設定して、非表示にします。
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
以上で、ステータス画像ビューのバインディング アダプターの設定が完了しました。次のステップでは、新しいバインディング アダプターを使用する画像ビューを追加します。
ステータス ImageView を追加する
このステップでは、先ほど定義したステータスを表示する画像ビューを fragment_overview.xml に追加します。
res/layout/fragment_overview.xmlを開きます。RecyclerView要素の下のConstraintLayout内に、下記のImageViewを追加します。
<ImageView
android:id="@+id/status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:marsApiStatus="@{viewModel.status}" />
上記の ImageView には、RecyclerView と同じ制約があります。ただし、幅と高さについては wrap_content が使用され、画像を拡大してビューを埋めるのではなく、中央に配置します。また、app:marsApiStatus 属性を viewModel.status に設定します。これにより、ViewModel のステータス プロパティが変更されたときに BindingAdapter が呼び出されます。
- 上記のコードをテストするには、エミュレータまたはデバイスで機内モードをオンにして、ネットワーク接続エラーをシミュレートします。アプリをコンパイルして実行します。エラー画像が表示されることを確認してください。

- 戻るボタンをタップしてアプリを閉じ、機内モードをオフにします。最近使ったアプリの画面を使用してアプリに戻ります。ネットワーク接続の速度によっては、画像の読み込みを開始する前にアプリがウェブサービスをクエリしている間、一瞬だけ読み込み中のアイコンが表示されることがあります。
おつかれさまです。これで Codelab は完了し、MarsPhotos アプリが完成しました。アプリを使って家族や友人に実際の火星の写真を見せてあげましょう。
この Codelab の解答コードは、下記のプロジェクトにあります。main ブランチを使用して、コードを取得またはダウンロードします。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン
をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを表示して、アプリがどのように実装されているかを確認します。
- Coil ライブラリを使用すると、アプリで画像を管理するプロセス(ダウンロード、バッファリング、デコード、キャッシュ保存など)を簡素化できます。
- バインディング アダプターは、ビューとビューのバインドされたデータをつなげる拡張メソッドです。バインディング アダプターは、データが変更されたときのカスタム動作を提供します。たとえば、Coil を呼び出して URL から
ImageViewに画像を読み込むことができます。 - バインディング アダプターは、
@BindingAdapterアノテーション付きの拡張メソッドです。 - 画像のグリッドを表示するには、
GridLayoutManagerでRecyclerViewを使用します。 - プロパティが変更されたときにプロパティのリストを更新するには、
RecyclerViewとレイアウトの間でバインディング アダプターを使用します。
Android デベロッパー ドキュメント:
その他:



