概要
前回の 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 デベロッパー ドキュメント:
その他: