Android Kotlin基礎講座 07.5:RecyclerViewのヘッダー

Android Kotlin基礎講座

タスク:RecyclerViewにヘッダーを追加する

ステップ1:DataIteクラスを作成する

アイテムの型を抽象化し、アダプターにただ単に”アイテム”として扱わせるためには、SleepNightとHeader両方を表すデータホルダークラスを作成します。データセットはデータホルダーアイテムのリストになります。

GitHubからスターターアプリを取得することもできますし、前回の記事で作成したTrackMySleepQualityアプリを引き続き使うこともできます。

  1. GitHubからRecyclerViewHeaders-Starterをダウンロードしtけうださい。
    RecyclerViewHeaders-Starterディレクトリに、この記事で必要なアプリのスターターバージョンが含まれています。また前回の記事で作成したアプリを引き続き使うこともできます。
  2. SleepNightAdapter.ktを開いてください。
  3. SleepNightListenerクラスの下のトップレベルに、データのアイテムを表すDataItemというsealedクラスを定義してください。

    sealdクラスは閉じられた型を定義しており、これはDataItemクラスの全てのサブクラスがこのファイル内に定義される必要があることを意味しています。
    結果として、サブクラスの数がコンパイラに知られます。アダプターを壊してしまう可能性があるDataItem型を定義するコードを別のファイルに追加することが不可能になります。
sealed class DataItem {

 }
  1. DataItemクラスのボディ中に、データアイテムの別の型を表す二つのクラスを定義してください。一つ目はSleepNightItemです。これはSleepNightのラッパーですので、sleepNightという値を一つ取ります。これをsealedクラスの一部にするために、DataItemを継承させてください。
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. 二つ目のクラスはHeaderです。これはヘッダーを表します。ヘッダーは実際のデータがないため、オブジェクトとして宣言することができます。つまりHeaderのインスタンスは一つしか存在しないということになります。こちらにもDataItemを継承させてください。
object Header: DataItem()
  1. DataItem内、クラスレベルに、abstract Longのidプロパティを定義してください。
    アダプターがDiffUtilを使ってアイテムが変更されたか、またどのように変わったかを決定するために、DiffItemCallBackはそれぞれのアイテムのidを知る必要があります。SleepNightItemとHeaderがこの抽象プロパティidをオーバーライドする必要があるため、エラーが表示されます。
abstract val id: Long
  1. SleepNightItem内でidをnightIdを返すようにオーバーライドしてください。
override val id = sleepNight.nightId
  1. Header内でLong.MIN_VALUEを返すようにidをオーバーライドしてください。これは非常に小さい数値(-2の63乗)です。ですので、これが存在するnightIdと衝突することはありません。
override val id = Long.MIN_VALUE
  1. 最終的なコードは以下のようになります。エラーなしでビルドできるはずです。
sealed class DataItem {
    abstract val id: Long
    data class SleepNightItem(val sleepNight: SleepNight): DataItem()      {
        override val id = sleepNight.nightId
    }

    object Header: DataItem() {
        override val id = Long.MIN_VALUE
    }
}

ステップ2:ヘッダー用のViewHolderを作成する

  1. TextViewを表示するheader.xmlという名の新規レイアウトリソースファイル内にヘッダー用のレイアウトを作成します。特筆すべき点がないので、コードだけ記します。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Sleep Results"
    android:padding="8dp" />
  1. “Sleep Results”をstringリソースに抽出し、header_textと名付けてください。
    (Sleep Resultにカーソルを当ててAlt+Enterを押し、Extract string resouseを選択)
<string name="header_text">Sleep Results</string>
  1. SleepNightAdapter.kt内、SleepNightAdapter中のViewHolderクラスの上に、TextViewHolderクラスを新しく作成してください。このクラスはtextview.xmlレイアウトをインフレートし、TextViewHolderインスタンスを返します。これは以前にも行っているので、コードのみ記します。ViewとRをインポートする必要があります。
    class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): TextViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = layoutInflater.inflate(R.layout.header, parent, false)
                return TextViewHolder(view)
            }
        }
    }

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

次に、SleepNightAdapterの宣言を更新する必要があります。ViewHolder型のみをサポートするのではなく、ビューホルダーのどの型でも使えるようにする必要があります。

アイテムの型を定義する

  1. SleepNightAdapter.kt内、トップレベル、import文の下かつSleepNightAdapterの上に、ビュー型用の二つの定数を定義してください。

    RecyclerViewはアイテムごとのビュー型を区別し、正しくビューホルダーを代入できるようにする必要があります。
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. SleepNightAdapter内でgetItemViewType()をオーバーライドするための関数を作成し、正しいヘッダー、またはアイテム定数を現在のアイテムの型に応じて返すようにします。
    Inside the SleepNightAdapter, create a function to override getItemViewType() to return the right header or item constant depending on the type of the current item.
override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
            is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
        }
    }

SleepNightAdapterの定義を更新する

  1. SleepNightAdapterの定義内の、ListAdapter用の最初の引数をSleepNightからDataItemに更新してください。
  2. SleepNightAdapterの定義内の、ListAdapterの汎用引数をSleepNightAdapter.ViewHolderからRecyclerView.ViewHolderに変更してください。いくつかエラーが表示されますが、クラスヘッダーは以下のようになります。
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

onCreateViewHolder()を更新する

  1. onCreateViewHolder()のシグネチャを変更して、RecyclerView.ViewHolderを返すようにしてください。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. onCreateViewHolder()メソッドの実装を拡張して、適切なビューホルダーをそれぞれのアイテム型に返すようにします。更新後のメソッドは以下のようになります。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
            ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
            else -> throw ClassCastException("Unknown viewType ${viewType}")
        }
    }

onBindViewHolder()を更新する

  1. onBindViewHolder()のパラメーター型をViewHolderからRecyclerView.ViewHolderに変更してください。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. holderがViewHolderだった場合のみにデータをビューホルダーに代入する条件文を追加してください。
    Add a condition to only assign data to the view holder if the holder is a ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. getItem()によって返されたオブジェクト型をDataItem.SleepNightItemにキャストしてください。最終的なonBindViewHolder()関数は以下のようになります。
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ViewHolder -> {
                val nightItem = getItem(position) as DataItem.SleepNightItem
                holder.bind(nightItem.sleepNight, clickListener)
            }
        }
    }

DiffUtilコールバックを更新する

  1. SleepNightDiffCallback内のメソッドを変更して、新しいDataItemクラスをSleepNightの代わりに使えるようにします。以下のコードで示されているように、SuppressLintで警告を抑制します。
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
    override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem.id == newItem.id
    }
    @SuppressLint("DiffUtilEquals")
    override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem == newItem
    }
}

ヘッダーを追加、サブミットする

  1. SleepNightAdapter内、onCreateViewHolder()の下に、以下のようにaddHeaderAndSubmitList()関数を定義してください。
    この関数はSleepNightのリストを取ります。リストをサブミットするためにListAdapterに準備されたsubmitList()を使う代わりに、この関数を使ってヘッダーを追加し、それからリストをサブミットします。
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. addHeaderAndSubmitList()内で、もし渡されたリストがnullであった場合にはヘッダーを返すだけにし、そうでなければヘッダーをリストの最初に入れ、それからリストをサブミットします。
val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
submitList(items)
  1. SleepTrackerFragment.ktを開き、submitList()の呼び出しをaddHeaderAndSubmitList()に変更してください。
  2. アプリを起動して、ヘッダーが睡眠データのリストの最初のアイテムとして、どのように表示されているかを観察してください。

プロフィール

プロフィール
コードラボJP

大学卒業後SEに就職、現在は退職しフリーランスとして活動中。
『初心者でも挫折せずに一人でプログラミングを学べる』をモットーに、コードラボJPを開設
お問い合わせ等はcodelabsjp@gmail.comまで

コードラボJPをフォローする
タイトルとURLをコピーしました