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 アプリを変更して、Mars プロパティ データから画像 URL を取得し、Glide を使用してその画像を読み込んで表示します。
  • 読み込み中のアニメーションとエラーアイコンをアプリに追加します。
  • RecyclerView を使用して火星の物件画像のグリッドを表示します。
  • ステータス処理とエラー処理を RecyclerView に追加します。

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

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

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

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

Glide には、基本的に次の 2 つのものが必要です。

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

このタスクでは、Glide を使用して、不動産ウェブサービスの画像を 1 つだけ表示する方法を説明します。ウェブサービスから返されたプロパティのリストで、最初の火星プロパティを表す画像を表示します。画像の表示前と表示後のスクリーンショットを次に示します。

ステップ 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. 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() メソッドで、_response.value をプロパティ数に設定する try/catch {} ブロック内の行を見つけます。下記のテストを追加します。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 をインポートします。

    イメージを pull するサーバーにはこのスキームが必要なため、最後の Uri オブジェクトに HTTPS スキームを使用します。HTTPS スキームを使用するには、buildUpon.scheme("https")toUri ビルダーの末尾に追加します。toUri() メソッドは、Android KTX コアライブラリの 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 の項目ごとに使用するレイアウト リソース ファイルです。ここで一時的に使用して、1 つの画像のみを表示します。
  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 を開いて、右側の [プレビュー] タブをクリックします。エラー画像としては、組み込みのアイコン ライブラリにある破損した画像のアイコンを使用します。このベクター型ドローアブルでは、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: ビューモデルを更新する

現在、ビューモデルには、1 つの MarsProperty オブジェクト(ウェブサービスからのレスポンス リストの最初のオブジェクト)を保持する _property LiveData があります。このステップでは、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: レイアウトとフラグメントを更新する

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

  1. res/layout/gridview_item.xml を開きます。データ バインディングを OverviewViewModel から MarsProperty に変更し、変数の名前を "property" に変更します。
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. <ImageView> で、MarsProperty オブジェクト内の画像 URL を参照するように app:imageUrl 属性を変更します。
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> 要素を追加します。これは、単一のアイテムに対して GridLayoutManager レイアウトと grid_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 レイアウトに 1 つの 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 クラスのコンパニオン オブジェクトの下に、MarsPropertyViewHolder の内部クラス定義を追加します。これは RecyclerView.ViewHolder を拡張します。

    リクエストされたときに 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() メソッドは、GridViewItemBinding をインフレートし、親 ViewGroup コンテキストの LayoutInflater を使用して作成された、新しい MarsPropertyViewHolder を返す必要があります。
   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 を開きます。app:listData 属性を RecyclerView 要素に追加し、データ バインディングを使用して 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. try {} ブロックの先頭に、await() を呼び出す前に 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 を開きます。RecyclerView 要素の下の ConstraintLayout 内に、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() メソッドを使用します。たとえば、読み込みドローアブルを指定するには apply()placeholder() を使用し、エラー ドローアブルを指定するには apply()error() を使用します。
  • 画像のグリッドを作成するには、RecyclerViewGridLayoutManager を使用します。
  • プロパティが変更されたときにプロパティのリストを更新するには、RecyclerView とレイアウトの間にバインディング アダプターを使用します。

Udacity コース:

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

その他:

このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。

  • 必要に応じて課題を割り当てます。
  • 宿題の提出方法を生徒に伝える。
  • 宿題を採点します。

教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。

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

次の質問に答えてください

問題 1

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

into()

with()

imageview()

apply()

質問 2

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

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

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

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

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

問題 3

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

LiveData に対して setBindingAdapter() メソッドを呼び出します。

▢ メソッドを、BindingAdapters.kt という Kotlin ファイルに追加します。

▢ XML レイアウト内で android:adapter 属性を使ってください。

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

次のレッスンを開始する: 8.3 インターネット データのフィルタリングと詳細表示

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