Android Kotlin基礎講座 08.2:インターネットから画像をロードし表示する

タスク:RecyclerViewで画像のグリッドを表示する

現在アプリはインターネットから物件情報を読み込んでいます。MarsPropetyリストの最初のアイテムのデータを利用して、ビューモデル内にproperty LiveDataを作成し、物件データからの画像URLを使ってImageViewを作成しました。しかし最終目標は画像のグリッドを表示することですので、GridLayoutManagerを用いたRecyclerViewを使う必要があります。

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

現在、ビューモデルにはウェブサービスから取得した結果のリストの一番最初であるMarsPropertyオブジェクトを保持している_property LiveDataがあります。このステップでは、そのLiveDataを変更して、MarsPropertyオブジェクトのリスト全体を保持できるようにします。

  1. overview/OverviewViewModel.ktを開いてください。
  2. _property変数を_propertiesに変更してください。型をMarsPropertyオブジェクトのリストに変更してください。
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. 外部用のproperty LiveDataもpropertiesに変更してください。ここでも同様にLiveDataの型にリストを追加してください。
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. getMarsRealEstateProperties()メソッドまでスクロールしてください。try{}ブロックの中の以前のタスクで追加したテスト全体を以下のコードで置き換えてください。
    MarsApi.retrofitService.getProperties()はMarsPropertyオブジェクトのリストを返すので、単純にそれを_properties.valueに代入してください。
_properties.value = MarsApi.retrofitService.getProperties()

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

try {
    _properties.value = MarsApi.retrofitService.getProperties()   
    _response.value = "Success: Mars properties retrieved"
} 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>内の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>要素を追加してください。これは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:PhotoGridAdapterを追加する

ここまででgrid_view_itemレイアウトが単体のImageViewを持っている一方で、fragment_overviewレイアウトはRecyclerViewを持つようになりました。このステップでは、RecyclerViewアダプターを通して、RecyclerViewにデータをバインドします。

Note: これは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用のcompanionオブジェクトの定義を追加してください。

要求されたら、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の参照演算子(===)を使ってください。これはオブジェクトのolItemの参照とnewItemの参照が同じである場合にtrueを返します。
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. areContentsTheSame()に対しては、olItemとnewItemのIDに対して、標準の等式演算子(==)を使ってください。
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. さらにPhotoGridAdapterクラス内、companionオブジェクトの下に、MarsPropertyViewHolder用の内部クラスの定義を追加してください。これはRecyclerView.ViewHolderを継承しています。

要求されたら、androidx.recyclerview.widget.RecyclerViewとcom.example.android.marsrealestate.databinding.GridViewItemBindingをインポートしてください。

レイアウトのMarsPropertyをバインドするために、GridViewItemBinding変数が必要になりますので、この変数をMarsPropertyViewHolderに渡します。ベースのViewHolderクラスはコンストラクタでビューを要求しているので、binding.rootビューを渡します。

class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {
}
  1. MarsPropertyViewHolder内にMarsPropertyオブジェクトを引数にとるbind()メソッドを作成し、そのオブジェクトにbinding.propertyをセットしてください。
    propertyのセットの後で、executePendingBindings()を呼び出してください。これは即座に更新を実行します。
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}

Note: この変更によってAndroid Studioでデータバインディングエラーが発生することがあります。これらのエラーを解消するには、アプリをクリーンアップしてリビルドしてください。

  1. onCreateViewHolder()内のTODOを削除し、以下のコードを追加してください。
    要求されたらandroid.view.LayoutInflaterをインポートしてください。

onCreateViewHolder()メソッドはGridViewItemBindingのインフレート及び親ViewGroupコンテクストを使用したことによって生成された、MarsPropertyViewHolderを返す必要があります。

   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. onBindViewHolder()メソッド内のTODOを削除し、以下のコードを追加してください。ここでは、現在のRecyclerViewの位置に紐づけられたMarsPropertyオブジェクトを取得するために、getItem()を呼び出し、それをMarsPropertyViewHolder内のbind()メソッドに渡しています。
val marsProperty = getItem(position)
holder.bind(marsProperty)

ステップ4:バインディングアダプターを追加し、パーツに接続する

最終的に、BindingAdapterを使ってMarsPropertyオブジェクトのリストでPhotoGridAdapterを初期化します。BindingAdapterを使ってRecyclerViewデータをセットすることによって、データバインディングに自動的にMarsPropertyオブジェクトのリスト用のLiveDataを監視させることができます。そしてMarsPropertyリストに変更があった際には自動的にバインディングアダプターが呼び出されます。

  1. BindingAdapters.ktを開いてください。
  2. ファイルの最後にRecyclerViewをMarsPropertyオブジェクトのリストを引数にとるbindRecyclerView()メソッドを追加してください。そのメソッドを@BindingAdapterをアノテーションしてください。

要求されたら、androidx.recyclerview.widget.RecyclerViewとcom.example.android.marsrealestate.network.MarsPropertyをインポートしてください。

@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. bindRecyclerView()関数内で、recyclerView.adapterをPhotoGridAdapterにキャストし、dataを使ってadapter.submitList()を呼び出してください。これは新しいリストが使用可能になったときにRecyclerViewに伝えます。

要求されたら、com.example.android.marsrealestate.overview.PhotGridAdapterをインポートしてください。

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.photosGridのRecyclerViewアダプターを新規PhotoGridAdapterオブジェクトに初期化します。
binding.photosGrid.adapter = PhotoGridAdapter()
  1. アプリを起動してください。MarsProperty画像のグリッドが表示されます。新しい画像を見るためにスクロールすると、画像を表示する前に読み込み処理中のアイコンが表示されます。機内モードをオンにすると、まだ読み込まれていない画像に対しては破損画像のアイコンが表示されます。