Android Kotlin基礎講座 07.1:RecyclerViewの基礎

タスク:RecyclerViewとAdapterを実装する

このタスクでは、レイアウトファイルにRecyclerViewを追加し、RecycleViewに睡眠データを対応させるためのAdapterをセットアップしていきます。

ステップ1:LayoutManagerを使ってRecyclerViewを追加する

このステップでは、fragment_sleep_tracker.xmlファイル中のScrollViewをRecyclerViewで置き換えます。

  1. GitHubからRecyclerViewFundamentals-Starterをダウンロードしてください。
  2. ビルドしてアプリを起動してください。シンプルなテキストとして、どのようにデータが表示されているかを確認しておいてください。
  3. fragment_sleep_tracker.xmlレイアウトファイルをデザインタブで開いてください。
  4. コンポーネントツリー中のScrollViewを削除してください。これによってScrollView中に含まれるTextViewも削除されます。
  5. パレットで、コンポーネントタイプのリストからContainersを見つけて、選択してください。
  6. パレットからRecyclerViewをコンポーネントツリーにドラッグしてください。RecyclerViewはConstarintLayoutの中に配置してください。
8c6cfd99d4237c7d.png
  1. 依存関係(Dependency)を追加するか確認ダイアログが表示された場合は、OKをクリックしてAndroid StudioがGradleファイルにRecycleViewの依存関係を追加できるようにしてください。これには数秒かかることがあります。
828133c3a2314dc7.png
  1. Module build.gradleファイルを開いてください。一番下までスクロールして、新しく追加された依存関係を確認してください。以下のようなコードがあると思います。(Android Studioのバージョンによって異なります)
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. fragment_sleep_tracker.xmlに戻ってください。
  2. テキストタブで表示して、以下のようなRecyclerViewのコードを探してください。
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />/>
  1. sleep_listというidを付与してください。
android:id="@+id/sleep_list"
  1. RecyclerViewがConstraintLayout内の画面の残り部分を占有するようにします。それをするためには、RecyclerViewの上辺をStartボタンに、底辺をClearボタンに、左右はそれぞれ制約付けてください。layout widthは0dpにしてください。レイアウトエディターからでも、以下のコードを使ってXMLコードからでも構いません。
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. RecyclerViewにLayoutManagerを追加します。全てのRecyclerViewはどのようにリスト内にアイテムを配置するかを伝えるためのLayoutManagerを必要とします。Androidはデフォルトでは横幅を最大に使い、垂直なリストにアイテムをならっべるLinearLayoutManagerを用意しています。
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. デザインタブに切り替え、追加した位置制約によって、RecyclerViewが利用可能なスペースをいっぱいに使って拡大していることを確認してください。
eef3940d35065b97.png

ステップ2:リストアイテムレイアウトとテキストビューホルダーを作成する

RecyclerViewはただのコンテナに過ぎません。このステップでは、レイアウトとRecyclerViewで表示するアイテム用のインフラを作成していきます。

できるだけ早く実際に動作するRecyclerViewを確認するために、最初は睡眠の質を数字として表示するだけの単純なリストアイテムを使います。そのためにはTextItemViewHolderというビューホルダーが必要になります。また、データ用のビューである、TextViewも必要になります。(後のステップではビューホルダーについてと、全ての睡眠データを表示する方法についてより詳しく学習します)

  1. text_item_view.xmlというレイアウトファイルを作成してください。後でテンプレートコードで置き換えることになりますので、ルート要素はなんでも構いません。
  2. text_item_view.xml内にある全てのコードを削除してください。
  3. スタートとエンドに16dpのパディングを持ち、テキストサイズが24spのTextViewを追加してください。幅は親要素にマッチするようにします。高さはコンテンツを包み込むようにします。このビューはRecyclerView内で表示されるので、ViewGroup内に配置する必要はありません。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. Util.ktを開いてください。最後までスクロールし、以下のコードを追加してください。これによってTextItemViewHolderクラスが作成されます。コードはファイルの一番下、最後の閉じ括弧のあとに追加してください。このビューホルダーは一時的なもので、後に置き換えることになるので、このコードはUtil.ktに含みます。
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. 促された場合は、android.widget.TextViewとandroidx.recyclerview.widget.RecyclerViewをインポートしてください。

ステップ3:SleepNightAdapterを作成する

RecyclerViewを実装する上で、肝となるタスクはアダプターの作成です。現在、アイテムビュー用のシンプルなビューホルダーと、それぞれのアイテム用のレイアウトがあります。これでアダプターも作ることができるようになっています。
アダプターはビューホルダーを作成し、それにRecyclerViewに表示するデータをあてはめてくれます。

  1. sleeptrackerパッケージ内に、SleepNightAdapterというKotlinクラスを新規作成してください。
  2. SleepNightAdapterクラスでRecyclerView.Adapterを継承させてください。このクラスがSleepNightオブジェクトをRecyclerViewが使えるように適合させてくれるので、SleepNightAdapterと名付けます。アダプターはどのビューホルダーを使うかを知る必要があるので、TextItemViewHolderを渡してください。必要に応じて必要なコンポーネントをインポートしてください。
    必ず実装しなければならないメソッドがあるので、現時点ではエラーが表示されます。
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. SleepNightAdapterのトップレベルにデータを保存するためのlistOf Sleepnight変数を作成してください。
var data = listOf<SleepNight>()
  1. SleepNightAdapter中で、getItemCount()をオーバーライドし、data中のsleep nightsのリストのサイズを返すようにします。RecyclerViewは表示するためのデータをアダプターがいくつ持っているかを知る必要があるので、getItemCount()を呼び出すことで、数を知るようにします。
override fun getItemCount() = data.size
  1. SleepNightAdapter中で、以下に示されているようにonBindViewHolder()関数をオーバーライドしてください。

onBindViewHolder()関数は特定の位置のリストアイテム用のデータを表示するためにRecyclerViewから呼び出されます。従ってonBindViewHolder()メソッドは二つの引数を取ります。ビューホルダーとデータの位置です。このアプリでは、ビューホルダーはTextItemViewHolder、位置はリスト中の位置とします。

override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {}
  1. onBindViewHolder()の中に、データ中の与えられた位置のアイテム用の変数を作成してください。
 val item = data[position]
  1. 作成済みのViewHolderはtextViewというプロパティを持っています。
    onBindViewHolder()中で、textViewのtextを睡眠の質を表す数字に設定します。このコードは数字のリストを表示するだけですが、このシンプルな例でどのようにアダプターがデータをビューホルダーに対応させ、画面上に表示するのかを学習していきます。
holder.textView.text = item.sleepQuality.toString()
  1. SleepNightAdapter中で、onCreateViewHolder()をオーバーライドし、実装してください。これはRecyclerViewがアイテムを表すためのビューホルダーを必要としたときに呼び出されます。

この関数は二つのパラメーターをとり、ViewHolderを返します。ビューホルダーを保持するビューグループである、parentパラメーターは常インRecyclerViewです。
viewTypeパラメーターは同じRecyclerView内に複数のビューが存在するときに使われます。例えば、テキストビューのリスト、画像、動画を同じRecyclerView内に置いた場合、onCreateViewHolder()関数はどのタイプのビューを使うかを知る必要があります。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {}
  1. onCreateViewHolder()内で、LayoutInflaterのインスタンスを作成してください。

レイアウトインフレーターはXMLレイアウトからビューをどのように作り出すかを知っています。contextにはビューをどのように正しくインフレートするかに関する情報が含まれています。リサイクラービュー用のアダプター内で、常にparentビューグループのcontextである、RecyclerViewを渡します。

val layoutInflater = LayoutInflater.from(parent.context)
  1. onCreateViewHolder()内で、layoutinflaterを利用してインフレートすることでviewを作成してください。

ビュー用のXMLレイアウトと、ビュー用のparentビューグループを渡してください。三つ目はboolean値であるattachToRootという引数です。この引数はfalseである必要があります。RecyclerViewはこのアイテムをビューヒエラルキーに追加するためです。

val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. onCreateViewHolder()内で、viewによって作られたTextItemViewHolderを返してください。
return TextItemViewHolder(view)
  1. アダプターはRecyclerViewにdataが変更された際に知らせる必要があります。なぜならRecyclerViewはdataについては何も知らないからです。RecyclerViewはアダプターが与えたビューホルダーについてのみ知っています。

RecyclerViewに表示しているデータが変更されたときに通知するためには、SleepNightAdapterクラスのトップにあるdata変数に独自セッターを追加します。セッターの中で、dataに新しい値を与え、新しいデータによってリストを再描写させるためにnotifyDataSetChanged()を呼び出してください。

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

Note: notifyDataSetChanged()が呼び出されると、RecyclerViewは変更されたアイテムのみではなく、リスト全体を再描写します。これはシンプルで、とりあえずは動作しますが、この記事のシリーズ中で、後にこのコードを改良します。

ステップ4:RecyclerViewにAdapterについて伝達する

RecyclerViewはビューホルダーを取得するためにアダプターについて知る必要があります。

  1. SleepTrackerFragment.ktを開いてください。
  2. onCreateView()中で、アダプターを作成します。以下のコードをViewModelの作成の後、かつreturn文の前に追加してください。
val adapter = SleepNightAdapter()
  1. adpterをRecyclerViewと紐づけてください。
binding.sleepList.adapter = adapter
  1. bindingオブジェクトを更新するために、プロジェクトをクリーンアップしてリビルドしてください。

もしbinding.sleepList、またはbinding.FragmentSleepTrackerBindingに関するエラーが表示されている場合は、キャッシュを無効にして、リスタートしてください。(File > Invalidate Caches / Restart)

アプリが起動できたらエラーはないということになりますが、Start、Stopをタップしてもデータは表示されません。

ステップ5:データをアダプターに送る

ここまででアダプターとアダプターからRecyclerViewにデータを送る手段を手にしました。ここからはViewModelからアダプターにデータを送る必要があります。

  1. SleepTrackerViewModelを開いてください。
  2. nights変数を見つけてください。これは表示するデータである睡眠データを保存しています。nights変数はデータベースでgetAllNights()を呼び出すことによってセットされています。
  3. 変数にアクセスする必要があるオブザーバーを作成するので、nightsからprivateを削除してください。コードは以下のようになります。
val nights = database.getAllNights()
  1. databaseパッケージ内のSleepDatabaseDaoを開いてください。
  2. getAllNights()関数を見つけてください。この関数はSleepNightのリストをLiveDataとして返していることを確認してください。これはnights変数にRoomによって更新され続けているLiveDataが含まれており、いつそれが変更されたかを知るためにnightsを監視することができるということを意味しています。
  3. SleepTrackerFragmentを開いてください。
  4. onCreateView()内、adapterを作成しているコードの下で、nights変数のオブザーバーを作成してください。

フラグメントのviewLifecycleOwnerをライフサイクルオーナーとして渡すことで、このオブザーバーがRecyclerViewが画面上にあるときのみアクティブになることを確実にすることができます。

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. このオブザーバーの中で、(nights用の)nullでない値を取得したら、それをアダプターのdataに代入します。以下がオブザーバー及びデータのセットの完成コードです。
    Inside the observer, whenever you get a non-null value (for nights), assign the value to the adapter’s data. This is the completed code for the observer and setting the data:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. ビルドしてアプリを起動してください。

アダプターが正常に動作していれば、睡眠の質を表す数値がリストとして表示されるはずです。左側の画像はStartをタップした後に-1が表示されている様子です。右側はStopボタンをタップし、質を評価したあとに値が更新されている様子を表しています。

89f71f10deda270.png

ステップ6:ビューホルダーがどのように使いまわされるかを確認する

RecyclerViewはビューホルダーを使いまわします。再利用しているということです。画面をスクロールすることによって、ビューが画面外にいったときに、RecyclerViewはそのビューを次に画面に入り込んでくる新しいビュー用に再利用します。

これらのビューホルダーは使いまわされるので、onBindViewHolder()が以前のアイテムがビューホルダーにセットしていたカスタマイズをセット、またはリセットしていることを確認してください。

例として、値が1以下である睡眠の質データを保持しているビューホルダーのテキストカラーを赤に設定し、よく眠れなかったことを表現することができます。

  1. SleepNightAdapterクラス内のonBindViewHolder()の最後に以下のコードを追加してください。
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. アプリを起動してください。
  2. 1以下の睡眠データを追加し、数字が赤で表示されることを確認してください。
  3. 高レートであるにも関わらず、赤い文字の数字が表示されるまで、高レートを追加してください。

RecyclerViewはビューホルダーを再利用しているので、最終的に赤いビューホルダーを高い数値にも再利用しているのです。誤って高数値が赤で表示されてしまっています。

b57e7915d6dd3c78.png
  1. これを修正するために、else文を追加し、質が1よりも大きい場合には、テキストカラーを黒に設定します。

両方の場合を明示的にすることによって、ビューホルダーは正しいテキストカラーをそれぞれのアイテムに利用することができます。

if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. アプリを起動し、数字が常に正しい色で表示されていることを確認してください。

おめでとうございます!ここまでで完全に機能する基本的なRecyclerViewが出来上がりました。