Android Kotlin の基礎 08.2: インターネットから画像を読み込んで表示する

この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。

はじめに

前回の Codelab では、ウェブサービスからデータを取得し、レスポンスを解析してデータ オブジェクトに変換する方法を学習しました。この Codelab では、その知識に基づいて、ウェブ URL から写真を読み込んで表示します。また、RecyclerView を作成して概要ページに画像のグリッドを表示する方法もおさらいします。

前提となる知識

  • フラグメントの作成方法と使用方法。
  • ビューモデル、ビューモデル ファクトリ、変換、LiveData などのアーキテクチャ コンポーネントの使用方法。
  • REST ウェブサービスから JSON を取得する方法と、Retrofit ライブラリおよび Moshi ライブラリを使用してそのデータを解析し、Kotlin オブジェクトに変換する方法。
  • RecyclerView を使用してグリッド レイアウトを作成する方法。
  • AdapterViewHolderDiffUtil の機能。

学習内容

  • Glide ライブラリを使用してウェブ URL から画像を読み込んで表示する方法。
  • RecyclerView とグリッド アダプターを使用して画像のグリッドを表示する方法。
  • 画像をダウンロードして表示する際に発生するエラーの処理方法。

演習内容

  • MarsRealEstate アプリを変更して、火星の不動産物件データから画像 URL を取得し、Glide を使用してその画像を読み込んで表示します。
  • 読み込み中のアニメーションとエラーアイコンをアプリに追加します。
  • RecyclerView を使用して火星の物件画像のグリッドを表示します。
  • ステータス処理とエラー処理を RecyclerView に追加します。

この Codelab(および関連する Codelab)では、火星で販売されている不動産物件を表示する MarsRealEstate というアプリを使用します。アプリはインターネット サーバーに接続して、価格や販売または賃貸が可能かどうかなどの詳細を含む不動産データを取得して表示します。各物件を表す画像は、NASA の火星探査機が撮影した火星の実際の写真です。

この Codelab で作成するバージョンのアプリは概要ページにデータを書き込みます。概要ページでは画像のグリッドを表示します。これらの画像は、アプリが Mars 不動産ウェブサービスから取得した物件データの一部です。アプリは Glide ライブラリを使用して画像の読み込みと表示を行い、RecyclerView を使用して画像のグリッド レイアウトを作成します。また、アプリはネットワーク エラーを適切に処理します。

ウェブ URL からの写真を表示するのは簡単に思えますが、適切に処理するにはかなりの作業が必要です。画像をダウンロードしてバッファリングし、圧縮形式から Android が使用できる形式にデコードする必要があります。メモリ内キャッシュとストレージベースのキャッシュの両方またはいずれかに画像を保存する必要があります。これらすべてを優先度の低いバックグラウンド スレッドで行いつつ、UI の応答性を維持する必要があります。また、ネットワークと CPU のパフォーマンスを最適化するため、複数の画像を一度に取得してデコードする必要もあります。ネットワークから画像を効果的に読み込む方法を学ぶことは、それ自体が Codelab になり得ます。

幸いなことに、コミュニティで開発された Glide というライブラリを使用して、画像のダウンロード、バッファリング、デコード、キャッシュ保存を行うことができます。Glide を使用すると、すべてをゼロから行うよりもはるかに手間が省けます。

Glide は、基本的に次の 2 つのものを必要とします。

  • 読み込んで表示する画像の URL。
  • 画像を表示するための ImageView オブジェクト。

このタスクでは、Glide を使用して、不動産ウェブサービスから取得した単一の画像を表示する方法を学びます。ウェブサービスから返された不動産物件のリストに含まれる最初の火星の不動産物件を表す画像を表示します。画像の表示前と表示後のスクリーンショットを次に示します。

ステップ 1: Glide の依存関係を追加する

  1. 前回の Codelab で作成した MarsRealEstate アプリを開きます。(アプリをお持ちでない場合は、MarsRealEstateNetwork をこちらからダウンロードできます)。
  2. アプリを実行して動作を確認します(火星で利用可能な仮想の物件のテキストの詳細が表示されます)。
  3. build.gradle(モジュール: app)を開きます。
  4. dependencies セクションで、Glide ライブラリ用に次の行を追加します。
implementation "com.github.bumptech.glide:glide:$version_glide"


バージョン番号は、プロジェクトの Gradle ファイルで個別に定義されています。

  1. [Sync Now] をクリックし、新しい依存関係でプロジェクトを再ビルドします。

ステップ 2: ビューモデルを更新する

次に、OverviewViewModel クラスを更新して、1 つの Mars プロパティのライブデータを含めます。

  1. overview/OverviewViewModel.kt を開きます。_responseLiveData のすぐ下に、単一の MarsProperty オブジェクトの内部(可変)ライブデータと外部(不変)ライブデータの両方を追加します。

    リクエストされたら、MarsProperty クラス(com.example.android.marsrealestate.network.MarsProperty)をインポートします。
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. getMarsRealEstateProperties() メソッドの try/catch {} ブロック内で、_response.value をプロパティの数に設定している行を見つけます。以下のテストを追加します。MarsProperty オブジェクトが利用可能な場合、このテストでは _property LiveData の値を listResult の最初のプロパティに設定します。
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

完全な try/catch {} ブロックは次のようになります。

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. res/layout/fragment_overview.xml ファイルを開きます。<TextView> 要素で、android:text を変更して property LiveDataimgSrcUrl コンポーネントにバインドします。
android:text="@{viewModel.property.imgSrcUrl}"
  1. アプリを実行します。TextView には、最初の火星プロパティの画像の URL のみが表示されます。ここまでの作業では、その URL 用にビューモデルとライブデータを設定しただけです。

ステップ 3: バインディング アダプターを作成して Glide を呼び出す

表示する画像の URL が取得できたので、Glide を使用してその画像を読み込む作業を開始します。このステップでは、バインディング アダプターを使用して、ImageView に関連付けられた XML 属性から URL を取得し、Glide を使用して画像を読み込みます。バインディング アダプターは、ビューとバインドされたデータをつなげる拡張メソッドで、データが変更されたときのカスタム動作を提供します。この場合、カスタム動作は Glide を呼び出して URL から ImageView に画像を読み込むことです。

  1. BindingAdapters.kt を開きます。このファイルは、アプリ全体で使用するバインディング アダプターを保持します。
  2. パラメータとして ImageViewString を受け取る bindImage() 関数を作成します。関数に @BindingAdapter アノテーションを付けます。@BindingAdapter アノテーションは、XML アイテムに imageUrl 属性がある場合にこのバインディング アダプターを実行するようデータ バインディングに指示します。

    リクエストされたら、androidx.databinding.BindingAdapterandroid.widget.ImageView をインポートします。
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. bindImage() 関数内で、imgUrl 引数に let {} ブロックを追加します。
imgUrl?.let { 
}
  1. let {} ブロック内に次の行を追加し、URL 文字列(XML から)を Uri オブジェクトに変換します。リクエストされたら androidx.core.net.toUri をインポートします。

    最終的な Uri オブジェクトで HTTPS スキームを使用します。これは、画像の取得元サーバーでこのスキームが必要になるためです。HTTPS スキームを使用するには、buildUpon.scheme("https")toUri ビルダーの末尾に追加します。toUri() メソッドは Android KTX Core ライブラリの Kotlin 拡張関数であるため、String クラスの一部のように見えます。
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. let {} 内で、Glide.with() を呼び出して、Uri オブジェクトから ImageView に画像を読み込みます。リクエストされたら、com.bumptech.glide.Glide をインポートします。
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

ステップ 4: レイアウトとフラグメントを更新する

Glide は画像を読み込みましたが、まだ何も表示されていません。次のステップでは、レイアウトとフラグメントを ImageView で更新して画像を表示します。

  1. res/layout/gridview_item.xml を開きます。これは、この Codelab の後半で RecyclerView の各アイテムに使用するレイアウト リソース ファイルです。ここでは、単一の画像のみを表示するために一時的に使用します。
  2. <ImageView> 要素の上にデータ バインディング用の <data> 要素を追加して、OverviewViewModel クラスにバインドします。
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. 新しい画像読み込みバインディング アダプターを使用するため、ImageView 要素に app:imageUrl 属性を追加します。
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. overview/OverviewFragment.kt を開きます。onCreateView() メソッドで、FragmentOverviewBinding クラスをインフレートする行をコメントアウトし、バインディング変数に割り当てます。これは一時的なものであり、後で戻ります。
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. 次の行を追加して、GridViewItemBinding クラスをインフレートします。リクエストされたら、com.example.android.marsrealestate. databinding.GridViewItemBinding をインポートします。
val binding = GridViewItemBinding.inflate(inflater)
  1. アプリを実行します。結果リストの最初の MarsProperty の画像の写真が表示されます。

ステップ 5: 読み込み中とエラーのシンプルな画像を追加する

Glide を使用すると、画像の読み込み中のプレースホルダ画像と、読み込みが失敗した場合(画像がない、画像が破損しているなど)のエラー画像を表示して、ユーザー エクスペリエンスを改善できます。このステップでは、バインディング アダプターとレイアウトにその機能を追加します。

  1. res/drawable/ic_broken_image.xml を開いて、右側の [Preview] タブをクリックします。エラー画像としては、組み込みのアイコン ライブラリにある破損した画像のアイコンを使用します。このベクター型ドローアブルでは、android:tint 属性によりアイコンがグレーに色付けされています。

  1. res/drawable/loading_animation.xml を開きます。このドローアブルは、<animate-rotate> タグで定義されたアニメーションです。アニメーションでは、中心点の周りで画像ドローアブル(loading_img.xml)が回転します。(アニメーションはプレビューでは確認できません)。

  1. BindingAdapters.kt ファイルに戻ります。bindImage() メソッドで、Glide.with() の呼び出しを更新して、load()into() の間で apply() 関数を呼び出すようにします。リクエストされたら com.bumptech.glide.request.RequestOptions をインポートします。

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

以上で、アプリはインターネットから物件情報を読み込むようになりました。最初の MarsProperty リストアイテムからのデータを使用して、ビューモデルに LiveData プロパティを作成し、そのプロパティ データからの画像 URL を使用して ImageView にデータを入力しました。しかし、アプリの目標は画像のグリッドを表示することです。そこで、GridLayoutManagerRecyclerView を使用します。

ステップ 1: ビューモデルを更新する

現在、ビューモデルには _property LiveData があり、ウェブサービスからのレスポンス リストに含まれる最初のオブジェクトである MarsProperty オブジェクトを保持しています。このステップでは、この LiveData を変更して、MarsProperty オブジェクトのリスト全体を保持するようにします。

  1. overview/OverviewViewModel.kt を開きます。
  2. 非公開の _property 変数を _properties に変更します。型を MarsProperty オブジェクトのリストに変更します。
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. 外部の property ライブデータを properties に置き換えます。ここでも、リストを LiveData 型に追加します。
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. 下にスクロールして getMarsRealEstateProperties() メソッドを表示します。try {} ブロック内で、前のタスクで追加したテスト全体を次の行に置き換えます。listResult 変数には MarsProperty オブジェクトのリストが保持されているため、レスポンスが成功したかどうかをテストする代わりに、_properties.value に割り当てることができます。
_properties.value = listResult

try/catch ブロック全体は次のようになります。

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

ステップ 2: レイアウトとフラグメントを更新する

次のステップでは、アプリのレイアウトとフラグメントを変更して、単一の画像ビューを使用する代わりに、RecyclerView とグリッド レイアウトを使用するようにします。

  1. res/layout/gridview_item.xml を開きます。データ バインディングを OverviewViewModel から MarsProperty に変更し、変数の名前を "property" に変更します。
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. <ImageView> 内で app:imageUrl 属性を変更して、MarsProperty オブジェクトに含まれる画像 URL を参照するようにします。
app:imageUrl="@{property.imgSrcUrl}"
  1. overview/OverviewFragment.kt を開きます。onCreateview() で、FragmentOverviewBinding をインフレートする行のコメント化を解除します。GridViewBinding をインフレートする行を削除するか、コメントアウトします。上記の変更により、前のタスクで行った一時的な変更が取り消されます。
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. res/layout/fragment_overview.xml を開きます。<TextView> 要素全体を削除します。
  2. 代わりに、次の <RecyclerView> 要素を追加します。この要素は、単一アイテムに GridLayoutManagergrid_view_item レイアウトを使用します。
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            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" />

ステップ 3: 写真グリッド アダプターを追加する

以上で、fragment_overview レイアウトに RecyclerView が含まれ、grid_view_item レイアウトに単一の ImageView が含まれるようになりました。このステップでは、RecyclerView アダプターを使用して、データを RecyclerView にバインドします。

  1. overview/PhotoGridAdapter.kt を開きます。
  2. 下記のコンストラクタ パラメータを使って、PhotoGridAdapter クラスを作成します。PhotoGridAdapter クラスは ListAdapter を拡張します。このクラスのコンストラクタには、リストアイテムの型、ビューホルダー、DiffUtil.ItemCallback 実装が必要です。

    リクエストされた場合は、androidx.recyclerview.widget.ListAdapter クラスと com.example.android.marsrealestate.network.MarsProperty クラスをインポートします。次のステップでは、このコンストラクタに欠けている(それによってエラーを引き起こしている)別の機能を実装します。
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. PhotoGridAdapter クラス内の任意の場所をクリックし、Control+i を押して、ListAdapter メソッド(onCreateViewHolder()onBindViewHolder())を実装します。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. 以下に示すように、PhotoGridAdapter クラス定義の最後で、追加したメソッドの後に、DiffCallback のコンパニオン オブジェクト定義を追加します。

    リクエストされたら、androidx.recyclerview.widget.DiffUtil をインポートします。

    DiffCallback オブジェクトは、比較したいオブジェクト(MarsProperty)の型を使って DiffUtil.ItemCallback を拡張します。
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Control+i を押して、このオブジェクトのコンパレータ メソッド(areItemsTheSame()areContentsTheSame())を実装します。
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. areItemsTheSame() メソッドの TODO を削除します。Kotlin の参照の等価性演算子(===)を使用します。oldItemnewItem のオブジェクト参照が同じ場合は true を返します。
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. areContentsTheSame() の場合は、oldItemnewItem の ID に対して標準の等価演算子を使用します。
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. PhotoGridAdapter クラス内で、コンパニオン オブジェクトの下に、RecyclerView.ViewHolder を拡張する MarsPropertyViewHolder の内部クラス定義を追加します。

    必要に応じて androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.databinding.GridViewItemBinding をインポートします。

    MarsProperty をレイアウトにバインドするために GridViewItemBinding 変数が必要なので、変数を MarsPropertyViewHolder に渡します。基本クラス ViewHolder ではコンストラクタ内にビューが必要なので、バインディング ルートビューを渡します。
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. MarsPropertyViewHolder 内で、MarsProperty オブジェクトを引数として受け取り、binding.property をそのオブジェクトに設定する bind() メソッドを作成します。プロパティを設定した後、executePendingBindings() を呼び出します。これにより、更新が直ちに実行されます。
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. onCreateViewHolder() で、TODO を削除して下記の行を追加します。リクエストされたら、android.view.LayoutInflater をインポートします。

    onCreateViewHolder() メソッドは新しい MarsPropertyViewHolder を返す必要があります。このビューホルダーは、GridViewItemBinding をインフレートし、親 ViewGroup コンテキストからの LayoutInflater を使用することにより、作成されます。
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. onBindViewHolder() メソッドで、TODO を削除して下記の行を追加します。ここで getItem() を呼び出し、現在の RecyclerView の位置に関連付けられている MarsProperty オブジェクトを取得して、そのプロパティを MarsPropertyViewHolderbind() メソッドに渡します。
val marsProperty = getItem(position)
holder.bind(marsProperty)

ステップ 4: バインディング アダプターを追加して要素を接続する

最後に、BindingAdapter を使用して、MarsProperty オブジェクトのリストで PhotoGridAdapter を初期化します。BindingAdapter を使用して RecyclerView のデータを設定すると、LiveDataMarsProperty オブジェクトのリストがあるかどうかがデータ バインディングによって自動的に監視されるようになります。MarsProperty リストが変更されると、バインディング アダプターが自動的に呼び出されます。

  1. BindingAdapters.kt を開きます。
  2. ファイルの末尾に、RecyclerViewMarsProperty オブジェクトのリストを引数として受け取る bindRecyclerView() メソッドを追加します。そのメソッドに @BindingAdapter アノテーションを付けます。

    リクエストされたら、androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.network.MarsProperty をインポートします。
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. bindRecyclerView() 関数内で、recyclerView.adapterPhotoGridAdapter にキャストし、データを使用して adapter.submitList() を呼び出します。これにより、新しいリストが利用可能であることが RecyclerView に通知されます。

リクエストされたら、com.example.android.marsrealestate.overview.PhotoGridAdapter をインポートします。

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. res/layout/fragment_overview.xml を開きます。RecyclerView 要素に app:listData 属性を追加し、データ バインディングを使用してそれを viewmodel.properties に設定します。
app:listData="@{viewModel.properties}"
  1. overview/OverviewFragment.kt を開きます。onCreateView()setHasOptionsMenu() の呼び出しの直前で、binding.photosGridRecyclerView アダプターを新しい PhotoGridAdapter オブジェクトに初期化します。
binding.photosGrid.adapter = PhotoGridAdapter()
  1. アプリを実行します。MarsProperty 画像のグリッドが表示されます。スクロールして新しい画像を表示すると、画像自体が表示される前に、読み込みが進行中であることを示すアイコンが表示されます。機内モードをオンにすると、まだ読み込まれていない画像が、破損した画像のアイコンとして表示されます。

MarsRealEstate アプリは、画像を取得できない場合、破損した画像のアイコンを表示します。しかし、ネットワーク接続がない場合は、空白の画面を表示します。

これはユーザー エクスペリエンスとしては不適切です。このタスクでは、基本的なエラー処理を追加して、何が起こっているかをユーザーが理解できるようにします。インターネットが利用できない場合、アプリは接続エラーアイコンを表示します。MarsProperty リストの取得中は、読み込み中のアニメーションを表示します。

ステップ 1: ビューモデルにステータスを追加する

まず、ウェブ リクエストのステータスを表す LiveData をビューモデルに作成します。考慮する必要があるステータスは、読み込み中、成功、失敗の 3 つです。「読み込み中」ステータスは、await() の呼び出しでデータを待機している状態を示します。

  1. overview/OverviewViewModel.kt を開きます。ファイルの先頭部分(インポートの後、クラス定義の前)に、すべての使用可能なステータスを表す enum を追加します。
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. OverviewViewModel クラス全体で、内部と外部の両方の _response ライブデータ定義の名前を _status に変更します。この Codelab の前半で _properties LiveData のサポートを追加したため、ウェブサービス レスポンス全体が未使用になっています。現在のステータスを追跡するには LiveData が必要です。既存の変数の名前を変更するだけで済みます。

また、型を String から MarsApiStatus. に変更します。

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. 下にスクロールして getMarsRealEstateProperties() メソッドを表示し、ここでも _response_status に更新します。"Success" 文字列を MarsApiStatus.DONE 状態に変更し、"Failure" 文字列を MarsApiStatus.ERROR に変更します。
  2. await() の呼び出しの前に、try {} ブロックの先頭に MarsApiStatus.LOADING ステータスを追加します。これは、コルーチンを実行してデータを待機しているときの初期ステータスです。完全な try/catch {} ブロックは次のようになります。
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. catch {} ブロックのエラー状態の後で、_properties LiveData を空のリストに設定します。これにより、RecyclerView がクリアされます。
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

ステップ 2: ステータス ImageView のバインディング アダプターを追加する

これでビューモデルにステータスが追加されましたが、これは単なる状態のセットです。アプリ自体に表示するにはどうすればよいですか?このステップでは、データ バインディングに接続された ImageView を使用して、読み込み中とエラーの状態を表すアイコンを表示します。アプリが読み込み中またはエラーの状態にあるときは、ImageView を表示する必要があります。アプリが読み込みを完了したときは、ImageView を非表示にする必要があります。

  1. BindingAdapters.kt を開きます。ImageViewMarsApiStatus 値を引数として受け取る bindStatus() という名前の新しいバインディング アダプターを追加します。リクエストされたら、com.example.android.marsrealestate.overview.MarsApiStatus をインポートします。
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. ステータスに応じて処理を切り替えるため、bindStatus() メソッド内に when {} を追加します。
when (status) {

}
  1. when {} 内に、「読み込み中」状態(MarsApiStatus.LOADING)の処理を追加します。この状態の場合は、ImageView を「表示」に設定し、読み込み中のアニメーションを割り当てます。これは、前のタスクで Glide について使用したのと同じアニメーション ドローアブルです。リクエストされたら、android.view.View をインポートします。
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. エラー状態(MarsApiStatus.ERROR)の処理を追加します。LOADING 状態の処理と同様に、ステータス ImageView を「表示」に設定し、接続エラー ドローアブルを再利用します。
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. 完了状態(MarsApiStatus.DONE)の処理を追加します。この場合は成功レスポンスが返されるので、ステータス ImageView の表示をオフにして、非表示にします。
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

ステップ 3: レイアウトにステータス ImageView を追加する

  1. res/layout/fragment_overview.xml を開きます。ConstraintLayout 内の RecyclerView 要素の下に、下記の ImageView を追加します。

    この ImageView には、RecyclerView と同じ制約があります。ただし、幅と高さについては wrap_content が使用され、画像を拡大してビューを埋めるのではなく、中央に配置します。また、app:marsApiStatus 属性にも注目してください。これにより、ビューモデルのステータス プロパティが変更されたときに、ビューが BindingAdapter を呼び出すようになります。
<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}" />
  1. エミュレータまたはデバイスで機内モードをオンにして、ネットワーク接続がない状態をシミュレートします。アプリをコンパイルして実行します。エラー画像が表示されることを確認してください。

  1. 戻るボタンをタップしてアプリを閉じ、機内モードをオフにします。最近使ったアプリの画面を使用してアプリに戻ります。ネットワーク接続の速度によっては、画像の読み込みを開始する前にアプリがウェブサービスをクエリしている間、一瞬だけ読み込み中のアイコンが表示されることがあります。

Android Studio プロジェクト: MarsRealEstateGrid

  • 画像を管理するプロセスを簡素化するには、Glide ライブラリを使用して、アプリで画像をダウンロード、バッファリング、デコード、キャッシュ保存します。
  • Glide がインターネットから画像を読み込むには、画像の URL と、画像を配置する ImageView オブジェクトの 2 つが必要です。これらのオプションを指定するには、Glide で load() メソッドと into() メソッドを使用します。
  • バインディング アダプターは、ビューとビューのバインドされたデータをつなげる拡張メソッドです。バインディング アダプターは、データが変更されたときのカスタム動作を提供します。たとえば、Glide を呼び出して URL から ImageView に画像を読み込むことができます。
  • バインディング アダプターは、@BindingAdapter アノテーション付きの拡張メソッドです。
  • Glide リクエストにオプションを追加するには、apply() メソッドを使用します。たとえば、placeholder() とともに apply() を使用して読み込み可能なドローアブルを指定し、error() とともに apply() を使用してエラー ドローアブルを指定します。
  • 画像のグリッドを生成するには、GridLayoutManagerRecyclerView を使用します。
  • プロパティが変更されたときにプロパティのリストを更新するには、RecyclerView とレイアウトの間でバインディング アダプターを使用します。

Udacity コース:

Android デベロッパー ドキュメント:

その他:

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

読み込まれた画像を含む ImageView を示すには、どの Glide メソッドを使用しますか?

into()

with()

imageview()

apply()

問題 2

Glide の読み込み時に表示するプレースホルダ画像を指定するには、どうすればよいですか。

▢ ドローアブルで into() メソッドを使用する。

RequestOptions() を使用し、ドローアブルで placeholder() メソッドを呼び出す。

Glide.placeholder プロパティをドローアブルに割り当てます。

RequestOptions() を使用し、ドローアブルで loadingImage() メソッドを呼び出す。

問題 3

メソッドがバインディング アダプターであることを示すには、どうすればよいですか。

LiveDatasetBindingAdapter() メソッドを呼び出します。

▢ メソッドを BindingAdapters.kt という Kotlin ファイルに配置する。

▢ XML レイアウトで android:adapter 属性を使用します。

▢ メソッドに @BindingAdapter アノテーションを付けます。

次のレッスン(8.3 インターネット データのフィルタリングと詳細表示)に進みます。

このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。