Android Kotlin の基礎 07.2: DiffUtil および RecyclerView とのデータ バインディング

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

はじめに

前の Codelab では、TrackMySleepQuality アプリを更新して、睡眠の質に関するデータを RecyclerView で表示しました。初めて RecyclerView を構築した際に学んだテクニックで、大きすぎないシンプルなリストを表示するほとんどの RecyclerViews で十分です。ただし、大規模なリストで RecyclerView を効率化し、複雑なリストとグリッドでコードの管理と拡張を簡単にする方法はいくつかあります。

この Codelab では、前の Codelab で作成した睡眠トラッカー アプリを使用します。睡眠データのリストを更新するより効果的な方法と、RecyclerView でデータ バインディングを使用する方法について学習します。(前の Codelab のアプリがインストールされていない場合は、この Codelab のスターター コードをダウンロードできます)。

前提となる知識

  • アクティビティ、フラグメント、ビューを使用した基本的なユーザー インターフェースの作成。
  • フラグメント間を移動し、safeArgs を使用してフラグメント間でデータを渡す。
  • モデル、モデル ファクトリ、変換、LiveData とそのオブザーバーを表示します。
  • Room データベースを作成し、DAO を作成してエンティティを定義する方法。
  • データベースや他の長時間実行タスクにコルーチンを使用する方法。
  • AdapterViewHolder、アイテム レイアウトを持つ基本的な RecyclerView を実装する方法。

学習内容

  • DiffUtil を使用して、RecyclerView によって表示されるリストを効率的に更新する方法。
  • RecyclerView でデータ バインディングを使用する方法。
  • バインディング アダプターを使用してデータを変換する方法。

演習内容

  • このシリーズの前の Codelab で使用した TrackMySleepQuality アプリに基づいて構築します。
  • DiffUtil を使用してリストを効率的に更新するように SleepNightAdapter を更新します。
  • バインディング アダプターを使用して RecyclerView のデータ バインディングを実装し、データを変換する。

睡眠トラッカー アプリには、次の図に示すように、フラグメントで表される 2 つの画面があります。

左側の最初の画面には、トラッキングを開始および停止するボタンがあります。画面にユーザーの睡眠データの一部が表示されます。[消去] ボタンをクリックすると、そのアプリがユーザーのために収集したすべてのデータが完全に削除されます。右の 2 番目の画面では、睡眠の質の評価を選択しています。

このアプリは、UI コントローラ、ViewModelLiveDataRoom データベースを使用して睡眠データを保持するように設計されています。

睡眠データは RecyclerView に表示されます。この Codelab では、RecyclerViewDiffUtil とデータ バインディングの部分を作成します。この Codelab が終了すると、アプリはまったく同じように見えますが、アプリはさらに効率的で、スケーリングやメンテナンスも簡単です。

前の Codelab の SleepTracker アプリを引き続き使用するか、GitHub から RecyclerViewDiffUtilDataBinding-Starter アプリをダウンロードしてください。

  1. 必要に応じて、GitHub から RecyclerViewDiffUtilDataBinding-Starter アプリをダウンロードし、Android Studio でプロジェクトを開きます。
  2. アプリを実行します。
  3. SleepNightAdapter.kt ファイルを開きます。
  4. コードを調べて、アプリの構造を把握します。下の図を参照して、RecyclerView とアダプター パターンの組み合わせを使用して睡眠データをユーザーに表示する方法をご確認ください。

  • アプリは、ユーザー入力から SleepNight オブジェクトのリストを作成します。各 SleepNight オブジェクトは、1 晩の睡眠、その時間、品質を表します。
  • SleepNightAdapterSleepNight オブジェクトのリストを RecyclerView が使用および表示できるものに適応させます。
  • SleepNightAdapter アダプターは、データを表示するためにリサイクラー ビューのビュー、データ、メタ情報を含む ViewHolders を生成します。
  • RecyclerViewSleepNightAdapter を使用して、表示する項目数(getItemCount())を決定します。RecyclerViewonCreateViewHolder()onBindViewHolder() を使用して、表示するデータにバインドされたビューホルダーを取得します。

notificationDataSetChanged() メソッドが非効率的

リスト内のアイテムが変更されて更新する必要があることを RecyclerView に通知するため、現在のコードでは、以下に示すように SleepNightAdapternotifyDataSetChanged() が呼び出されます。

var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

一方、notifyDataSetChanged() は、リスト全体が無効である可能性があることを RecyclerView に伝えます。その結果、RecyclerView は、リストに表示されないすべてのアイテム(画面に表示されないアイテムを含む)を再バインドして再描画します。これは多くの不要な作業です。大規模なリストや複雑なリストの場合、ユーザーがリストをスクロールしたときにディスプレイがちらつく、途切れる処理にかなり時間がかかることがあります。

この問題を修正するには、RecyclerView に正確な変更内容を伝えます。RecyclerView は、画面で変更されたビューのみ更新できます。

RecyclerView には、単一の要素を更新するためのリッチな API があります。notifyItemChanged() を使用すると、アイテムが変更されたことを RecyclerView に通知できます。また、追加、削除、移動されたアイテムにも同様の関数を使用できます。すべて手動で行うこともできますが、タスクは簡単であり、相当なコードが必要になる可能性があります。

幸いなことに、これはより良い方法です。

DiffUtil は効率的で、多くの労力をかける

RecyclerView には、2 つのリスト間の差分を計算するための DiffUtil というクラスがあります。DiffUtil さんは古いリストと新しいリストを取得して、相違点を確認できます。追加、削除、変更されたアイテムを検索します。次に、Eugene W. Myers の差分アルゴリズムを使用して、新しいリストを生成するために古いリストから最低限の変更を行う必要がある。

DiffUtil によって変更が検出されたら、RecyclerView はその情報を使用して、変更、追加、削除、移動された項目のみを更新できるため、リスト全体をやり直すよりも効率的に処理できます。

このタスクでは、SleepNightAdapter をアップグレードして DiffUtil を使用し、データの変更に合わせて RecyclerView を最適化します。

ステップ 1: SleepNightDiffCallback を実装する

DiffUtil クラスの機能を使用するには、DiffUtil.ItemCallback を拡張する必要があります。

  1. SleepNightAdapter.kt を開きます。
  2. SleepNightAdapter の完全なクラス定義の下に、DiffUtil.ItemCallback を拡張する SleepNightDiffCallback という新しいトップレベル クラスを作成します。汎用パラメータとして SleepNight を渡します。
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
  1. SleepNightDiffCallback クラス名にカーソルを置きます。
  2. Alt+EnterOption+EnterMac の場合)そして メンバーを実装するを選択します。
  3. 表示されるダイアログで、Shift キーを押しながら左クリックで areItemsTheSame() メソッドと areContentsTheSame() メソッドを選択し、[OK] をクリックします。

    これにより、以下のように 2 つのメソッドの SleepNightDiffCallback 内にスタブが生成されます。DiffUtil は、これら 2 つのメソッドを使用してリストとアイテムがどのように変化したかを確認します。
    override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
  1. areItemsTheSame() 内の TODO を、渡された 2 つの SleepNight アイテム(oldItemnewItem)が同じかどうかをテストするコードに置き換えます。複数の商品アイテムに同じ nightId がある場合、その商品アイテムは同じであるため、true を返します。それ以外の場合は false を返します。DiffUtil では、このテストを使用して、商品アイテムが追加、削除、移動されたかどうかを確認できます。
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem.nightId == newItem.nightId
}
  1. areContentsTheSame() 内で、oldItemnewItem に同じデータ、つまり等しいかどうかを確認します。この等価チェックでは、SleepNight がデータクラスであるため、すべてのフィールドをチェックします。Data クラスでは、equals と他のメソッドを自動的に定義します。oldItemnewItem の間に差異がある場合、このコードは DiffUtil に項目が更新されたことを通知します。
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem == newItem
}

これは、RecyclerView を使用して変更リストを表示するための一般的なパターンです。RecyclerView には、リストでサポートされる RecyclerView アダプターの作成に役立つアダプター クラス ListAdapter が用意されています。

ListAdapter はリストを追跡し、リストが更新されるとアダプターに通知します。

ステップ 1: ListAdapter を拡張するようにアダプターを変更する

  1. SleepNightAdapter.kt ファイルで、SleepNightAdapter のクラス署名を変更して、ListAdapter を拡張します。
  2. メッセージが表示されたら、androidx.recyclerview.widget.ListAdapter をインポートします。
  3. SleepNightAdapter.ViewHolder の前に、SleepNight を最初の引数として ListAdapter に追加します。
  4. パラメータとして SleepNightDiffCallback() をコンストラクタに追加します。ListAdapter は、これを使用してリスト内の変更点を把握します。完成した SleepNightAdapter クラス署名は次のようになります。
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. SleepNightAdapter クラス内で、セッターを含む data フィールドを削除します。ListAdapter がリストを管理するので、必要がなくなります。
  2. ListAdapter はこのメソッドを実装しているため、getItemCount() のオーバーライドを削除します。
  3. onBindViewHolder() のエラーを取り除くには、item 変数を変更します。data を使用して item を取得する代わりに、ListAdapter が提供する getItem(position) メソッドを呼び出します。
val item = getItem(position)

ステップ 2: submitList() を使用してリストを常に最新の状態にする

変更されたリストが利用可能になったら、コードで ListAdapter に通知する必要があります。ListAdapter には、新しいバージョンのリストが利用可能であることを ListAdapter に通知するための submitList() というメソッドが用意されています。このメソッドが呼び出されると、ListAdapter は、新しいリストを古いリストと比較して、追加、削除、移動、変更された項目を検出します。その後、ListAdapterRecyclerView が表示する項目を更新します。

  1. SleepTrackerFragment.kt を開きます。
  2. onCreateView()sleepTrackerViewModel のオブザーバーで、削除した data 変数が参照されているエラーを見つけます。
  3. adapter.data = itadapter.submitList(it) の呼び出しに置き換えます。更新されたコードは以下のとおりです。

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.submitList(it)
   }
})
  1. アプリを実行します。リストが小さい場合は、それほど高速に処理されません。

このタスクでは、前の Codelab と同じ手法を使用してデータ バインディングを設定し、findViewById() への呼び出しを省きます。

ステップ 1: レイアウト ファイルにデータ バインディングを追加する

  1. [Text] タブで list_item_sleep_night.xml レイアウト ファイルを開きます。
  2. ConstraintLayout タグにカーソルを置き、Alt+Enter(Mac では Option+Enter)を押します。インテント メニュー(「クイック修正」メニュー)が開きます。
  3. [Convert to data binding layout] を選択する。これにより、レイアウトが <layout> にラップされ、内部に <data> タグが追加されます。
  4. 必要に応じて、前方に戻り、<data> タグ内で sleep という名前の変数を宣言します。
  5. typeSleepNight の完全修飾名 com.example.android.trackmysleepquality.database.SleepNight にします。完成した <data> タグは次のようになります。
   <data>
        <variable
            name="sleep"
            type="com.example.android.trackmysleepquality.database.SleepNight"/>
    </data>
  1. Binding オブジェクトの作成を強制するには、[Build > Clean Project] を選択して、[Build > Rebuild Project] を選択します。(問題が解決しない場合は、[ファイル] > [キャッシュを無効にする / 再起動] を選択します)。ListItemSleepNightBinding バインディング オブジェクトが、関連コードとともに、プロジェクトに生成されたファイルに追加されます。

ステップ 2: データ バインディングを使用してアイテム レイアウトをインフレートする

  1. SleepNightAdapter.kt を開きます。
  2. ViewHolder クラス内で、from() メソッドを見つけます。
  3. view 変数の宣言を削除します。

削除するコード:

val view = layoutInflater
       .inflate(R.layout.list_item_sleep_night, parent, false)
  1. 以下のように、view 変数が存在するときに、ListItemSleepNightBinding バインディング オブジェクトをインフレートする binding という新しい変数を定義します。バインディング オブジェクトに必要なインポートを行います。
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
  1. 関数の最後で view を返すのではなく、binding を返します。
return ViewHolder(binding)
  1. エラーを解消するには、「binding」という単語の上にカーソルを移動します。Alt+Enter(Mac では Option+Enter)を押して、インテント メニューを開きます。
  1. [Change parameter 'itemView' type of primary primary コンストラクタ of class 'ViewHolder' to 'ListItemSleepNightBinding'] を選択します。これにより、ViewHolder クラスのパラメータの型が更新されます。

  1. ViewHolder のクラス定義までスクロールすると、署名の変更が表示されます。from() メソッドで itemViewbinding に変更したため、itemView のエラーが表示されます。

    ViewHolder クラスの定義で、itemView のオカレンスの 1 つを右クリックし、[リファクタリング] > [名前を変更] を選択します。名前を「binding」に変更します。
  2. コンストラクタのパラメータ binding の前に val を付けて、プロパティにします。
  3. 親クラス RecyclerView.ViewHolder の呼び出しで、パラメータを binding から binding.root に変更します。View を渡す必要があり、binding.root はアイテム レイアウトのルート ConstraintLayout です。
  4. 完成したクラス宣言は以下のコードのようになります。
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){

また、findViewById() の呼び出しに関するエラーが表示され、修正します。

ステップ 3: findViewById() を置き換える

findViewById() ではなく binding オブジェクトを使用するように、sleepLengthqualityqualityImage プロパティを更新できるようになりました。

  1. 以下に示すように、binding オブジェクトのビューを使用するように、sleepLengthqualityStringqualityImage の初期化を変更します。その後は、コード内にエラーが表示されなくなります。
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage

バインディング オブジェクトを使用すると、sleepLengthqualityqualityImage の各プロパティを定義する必要がなくなります。DataBinding はルックアップをキャッシュに保存するため、これらのプロパティを宣言する必要はありません。

  1. sleepLengthqualityqualityImage の各プロパティ名を右クリックします。[インラインでリファクタリング] を選択するか、Control+Command+N(Mac では Option+Command+N)を押します。
  2. アプリを実行します(エラーが発生した場合は、プロジェクトをクリーンアップして再ビルドする必要があります)。

このタスクでは、データ バインディングとバインディング アダプターを使用するようにアプリをアップグレードし、ビューにデータを設定します。

前の Codelab では、Transformations クラスを使用して LiveData を取得し、テキストビューに表示する書式付き文字列を生成しました。ただし、異なる型や複雑な型をバインドする必要がある場合は、バインディング アダプターを提供して、データ バインディングでそれらの型を使用できるようにします。バインディング アダプターは、データを取り込んでビューやテキスト、画像などのビューをバインドできるものに適応させるアダプターです。

高品質の画像用とテキスト フィールド用の 3 つのバインディング アダプターを実装します。まとめると、バインディング アダプターを宣言するには、アイテムとビューを受け取るメソッドを定義し、@BindingAdapter アノテーションを付けます。メソッドの本文で、変換を実装します。Kotlin では、データを受信するビュークラスで拡張関数としてバインディング アダプターを作成できます。

ステップ 1: バインディング アダプターを作成する

このステップでは多数のクラスをインポートする必要があり、個別に呼び出されることはありません。

  1. SleepNightAdapater.kt を開きます。
  2. ViewHolder クラス内で bind() メソッドを見つけ、このメソッドの動作を確認します。ここでは、binding.sleepLengthbinding.qualitybinding.qualityImage の値を計算するコードを取得し、アダプター内で使用します。(当面は、コードをそのままにしておき、後のステップで移動します)。
  3. sleeptracker パッケージで、BindingUtils.kt というファイルを作成して開きます。
  4. TextViewsetSleepDurationFormatted という拡張関数を宣言し、SleepNight を渡します。この関数は、睡眠時間を計算してフォーマットするためのアダプターになります。
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
  1. setSleepDurationFormatted の本文で、ViewHolder.bind() の場合と同じようにデータをビューにバインドします。convertDurationToFormatted() を呼び出して、TextViewtext を書式付きテキストに設定します。(これは TextView の拡張関数であるため、text プロパティに直接アクセスできます)。
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
  1. このバインディング アダプターについてデータ バインディングを指定するには、関数に @BindingAdapter アノテーションを付けます。
  2. この関数は sleepDurationFormatted 属性のアダプターであるため、sleepDurationFormatted を引数として @BindingAdapter に渡します。
@BindingAdapter("sleepDurationFormatted")
  1. 2 つ目のアダプターは、SleepNight オブジェクトの値に基づいて睡眠のクオリティを設定します。TextViewsetSleepQualityString() という拡張関数を作成し、SleepNight を渡します。
  2. 本文で、ViewHolder.bind() の場合と同じようにデータをビューにバインドします。convertNumericQualityToString を呼び出して、text を設定します。
  3. 関数に @BindingAdapter("sleepQualityString") アノテーションを付けます。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
   text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
  1. 3 つ目のバインディング アダプターでは、画像ビューに画像を設定します。以下に示すように、ImageView に拡張関数を作成して setSleepImage を呼び出し、ViewHolder.bind() のコードを使用します。
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
   setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

ステップ 2: SleepNightAdapter を更新する

  1. SleepNightAdapter.kt を開きます。
  2. bind() メソッド内のデータをすべて削除します。これにより、データ バインディングと新しいアダプターを使用できるようになりました。
fun bind(item: SleepNight) {
}
  1. bind() 内でスリープを item に割り当てます。これは、新しい SleepNight についてバインディング オブジェクトに指示する必要があるためです。
binding.sleep = item
  1. その行の下に binding.executePendingBindings() を追加します。この呼び出しは、保留中のバインディングをすぐに実行するようにデータ バインディングを要求する最適化です。RecyclerView でバインディング アダプターを使用する場合は、ビューのサイズを少し速くするため、常に executePendingBindings() を呼び出すことをおすすめします。
 binding.executePendingBindings()

ステップ 3: XML レイアウトにバインディングを追加する

  1. list_item_sleep_night.xml を開きます。
  2. ImageView に、イメージを設定するバインディング アダプターと同じ名前の app プロパティを追加します。下記のように sleep 変数を渡します。

    このプロパティは、アダプターを介してビューとバインディング オブジェクトの間の接続を作成します。sleepImage が参照されると、アダプターは常に SleepNight からのデータを適応させます。
app:sleepImage="@{sleep}"
  1. sleep_length テキストビューと quality_string テキストビューについても同じ操作を行います。sleepDurationFormatted または sleepQualityString が参照されるたびに、アダプターは SleepNight からのデータを適応させます。
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
  1. アプリを実行します。以前とまったく同じように動作します。バインディング アダプターは、データの変更に合わせてビューのフォーマットと更新をすべて処理し、ViewHolder を簡素化してコードを以前よりもはるかに優れた構造にします。

過去いくつかのエクササイズに同じリストが表示されています。これは、Adapter インターフェースを使用して、さまざまな方法でコードを設計できることを示すためのものです。コードが複雑になるほど、適切に設計することが重要になります。本番環境アプリでは、これらのパターンなどが RecyclerView で使用されます。パターンはすべて機能し、それぞれにメリットがあります。どちらを選択するかは、構築内容によって異なります。

お疲れさまでした。この時点で、Android で RecyclerView を習得できます。

Android Studio プロジェクト: RecyclerViewDiffUtilDataBinding

DiffUtil:

  • RecyclerView には、2 つのリスト間の差分を計算するための DiffUtil というクラスがあります。
  • DiffUtil には ItemCallBack というクラスがあり、2 つのリストの違いを理解するために拡張されています。
  • ItemCallback クラスでは、areItemsTheSame() メソッドと areContentsTheSame() メソッドをオーバーライドする必要があります。

ListAdapter:

  • 一部のリストを無料で管理するには、RecyclerView.Adapter ではなく ListAdapter クラスを使用します。ただし、ListAdapter を使用する場合は、他のレイアウト用に独自のアダプタを作成する必要があります。そのため、この Codelab ではその方法について説明します。
  • Android Studio でインテント メニューを開くには、コードの任意の項目にカーソルを合わせ、Alt+Enter(Mac では Option+Enter)を押します。このメニューは、コードのリファクタリングやメソッド実装のスタブの作成に特に役立ちます。メニューはコンテキスト依存であるため、正しいメニューを取得するには、カーソルを正確に移動する必要があります。

データ バインディング:

  • アイテム レイアウトのデータ バインディングを使用して、データをビューにバインドする。

バインディング アダプター:

  • 以前に Transformations を使用してデータから文字列を作成しました。異なるデータ型や複雑な型のデータをバインドする必要がある場合は、データ バインディングで使用できるようにバインディング アダプターを用意します。
  • バインディング アダプターを宣言するには、アイテムとビューを受け取るメソッドを定義し、そのメソッドに @BindingAdapter アノテーションを付けます。Kotlin では、バインディング アダプターを View の拡張関数として記述できます。アダプタで適応するプロパティの名前を渡します。次に例を示します。
@BindingAdapter("sleepDurationFormatted")
  • XML レイアウトで、バインディング アダプターと同じ名前の app プロパティを設定します。データとともに変数を渡します。次に例を示します。
.app:sleepDurationFormatted="@{sleep}"

Udacity のコース:

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

その他のリソース:

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

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

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

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

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

問題 1

DiffUtil を使用するために必要なものは次のうちどれですか。該当するものをすべて選択してください。

ItemCallBack クラスを拡張する

areItemsTheSame() をオーバーライドします。

areContentsTheSame() をオーバーライドします。

▢ データ バインディングを使って項目間の差異を追跡します。

質問 2

バインディング アダプターの説明として正しいものは次のうちどれですか。

▢ バインディング アダプターは @BindingAdapter アノテーション付きの関数です。

▢ バインディング アダプターを使用すると、データ形式をビューホルダーから分離できます。

▢ バインディング アダプターを使用するには、RecyclerViewAdapter を使用する必要があります。

▢ バインディング アダプターは複雑なデータを変換する必要がある場合に適したソリューションです。

質問 3

バインディング アダプターの代わりに Transformations を使用する必要があるのはどのような場合ですか?該当するものをすべて選択してください。

▢ データはシンプルです。

▢ 文字列を書式設定しています。

▢ リストが長すぎる。

ViewHolder にはビューが 1 つしかありません。

次のレッスンを開始する: 7.3: RecyclerView を使った GridLayout