カテゴリー
Kotlin言語

ゼロから始めるAndroidアプリ(Kotlin編)Unit2 – 2 第3回 後半:RecyclerView(アダプタの作成)

Android Developers > スタートガイド > Kotlin と Android をゼロから学ぶ > Unit2 – レイアウトUnit2-2 第3回 後半 の内容をまとめたものです(翻訳ではありません)


Unit2 – 2 第3回 の後半です。アプリの画面に RecyclerView を設置、アダプタを作成して RecyclerView に取り付けて完成させますね

アプリに RecyclerView を追加する

  • RecyclerView をレイアウトファイル(activity_main.xml)に追加します
  • レイアウト(ViewGroup)には RecyclerView だけが子Viewとして入ります。デフォルトの ConstraintLayout よりもシンプルな FrameLayout に変更します

レイアウトを FrameLayout に変更します

まずレイアウトを変更します

  • activity_main.xml を開きます
  • Design Editor を Split(または Code)表示にして XML を次のように書き換えます
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
</FrameLayout>

レイアウトに RecyclerView を追加します

画面に RecyclerView を落とすだけ

  • Design 表示に切り替えます
  • Palette から Containers を選んで、その中から RecyclerView を選択してレイアウトにドロップします
  • Add Project Dependency というポップアップがでるのでOKします
    • ポップアップが出なければ何もしません
  • レイアウト画面に RecyclerView が出てくるまでしばらく待ちます
  • RecyclerView の layout_width と layout_height を match_parent にしておきます
    • RecyclerView が画面いっぱいに表示されるようにします
  • RecyclerView のリソースIDは recycler_view とします

RecyclerView の設定

XMLコードを書き換えて RecyclerView の設定をしますね

  • RecyclerView はリニアリストやグリッドのような表示方法をサポートしています
  • 項目(アイテム)の配置は LayoutManager が行います。今回のアプリではリストを縦方向に表示する LinearLayoutManager を使います
    • LayoutManagerは Androidフレームワーク で用意されています
    • ほかにも GridLayoutManager などがあります

レイアウトマネージャの設定

  1. Code 表示に戻ります
  2. XMLコードで、RecyclerView の中にあるレイアウトマネージャの箇所を探します
  3. 以下のように LinearLayoutManager を使うように設定します
app:layoutManager="LinearLayoutManager"

スクロールバーの設定

  • スクリーンサイズより長いリストを縦方向にスクロールできるように縦方向のスクロールバーを追加します
  • RecyclerView の中に次の属性を追加します
android:scrollbars="vertical"

RecyclerView 設定のまとめ

activity_main.xml は、こんな感じになります

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager" />

</FrameLayout>

アプリを実行してみる

とりあえず実行しちゃいます

  • まだ何もデータをセットしていないので何も表示されません
  • エラーがなければ RecyclerView の設置は成功しています

ビューホルダーを表示するためのレイアウトの作成

ビューホルダー用のレイアウトを作りますね

  • RecyclerView に表示されるリストの項目(アイテム)部分は、レイアウトを別に用意します
  • list_item.xml という名前のレイアウトファイルを作成します
  • これからつくるアプリのリストに表示されるのはテキストメッセージ(文字列)だけです。レイアウトには TextView をひとつ設置することにします

list_item.xml の作成

  1. Project で res > layout とすすんで右クリックします
  2. New > Layout Resource File と進んでファイル名に list_item を指定し、OK をクリックします
  3. list_item.xml という名前のファイルが作成されます
  4. list_item.xml を開いて Code 表示にします
  5. 以下のような XML を記述して TextView を設置します。id は item_title にします
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  • layout_width と layout_height には wrap_content を設定しています

注意

  • このレイアウトは ViewGroup で囲う必要がありません
  • このレイアウトはオブジェクト化(inflate)されたあと RecyclerView の子として追加されます

list_item.xml を作成する別の方法

ルートになる部品(View)を指定して、正しいひな型を最初に作ってもらっちゃうんですね

  1. Project で res > layout とすすんで右クリック、または File > New > Layout Resource File と選択します
    • File Name: list_item.xml
    • Root element: TextView
  2. OK ボタンをクリックしてファイルを作成します
  3. list_item.xml を開いてから上記のような必要な修正を行います

アダプタを作成する

RecyclerView 作成でちょっとややこしいのがアダプタの作成なの。がんばりますね

  • アダプタ(Adapter)は、データを RecyclerView で利用できる形にするための設計手法(design pattern)です
  • RecyclerView が画面表示に必要なデータをアダプタに要求し、アダプタはそれに応えてデータを整えます
  • アダプタはクラスとして作成し、そのインスタンスを RecyclerView の adapter プロパティに設定します
  • 今回は Datasource クラスから取得したデータを RecyclerView で表示します。このためのアダプタを作成します

アダプタのKotlinコード

  • アダプタは複数のパーツからできています
  • いままで作ってきたものにくらべてコードの量も多く複雑です
  • 最初は細かいことは気にせずにがんばりましょう
  • アプリを完成させれば、すべてのパーツがどんな風にうまく組み合わさっているのかよりよくわかるはず
  • このアプリのコードを基本にして、RecyclerView を使ったアプリが作れるようになります

ItemAdapter クラスを作成する

まずアダプタにするクラスを入れるファイルを作成しますね

クラスのパッケージは adapter にするんですね

このアプリのアダプタとして ItemAdapter クラスを作成します。このクラスは新規に作成する adapter パッケージに入れます

ItemAdapter.kt の作成

  1. Project で app > java > com.example.affirmations とたどって右クリック
  2. New > Package を選択します
  3. パッケージ名の最後に adapter を加えて Enter します
  4. 作成した adapter パッケージを右クリックします
  5. New > Kotlin File/Class と選択します
  6. class name: ItemAdapter としてクラスを作成します
  7. 作成した ItemAdapter.kt を開きます

データソース(アプリに表示するテキストメッセージ)を受け取るために ItemAdapter クラスのコンストラクタにパラメータを追加します

クラスの宣言はこんなかんじです

import com.example.affirmations.model.Affirmation

class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

}
  • データソースは dataset 変数に代入します
  • dataset 変数は List<Affirmation> 型です
  • dataset 変数はクラス内でのみ使われるので private とします

Context オブジェクトについて

context ってときどき見かけるかも…

  • Android ではアプリ周りのさまざまな情報が Context オブジェクトのインスタンスに保存されています
  • ItemAdapter クラスには、この Context 型の変数 context も追加します
  • 変数 context はコンストラクタの先頭に置くようにします

ビューホルダーを作成する

ビューホルダーはアダプタの中につくるの

  • RecyclerView のリストに表示されるひとつひとつの項目(アイテム)はビューホルダーによって設定されます。RecyclerView は表示されている項目(アイテム)を直接操作しません
  • ビューホルダーと画面に表示されている項目(アイテム)は対応しています
  • 画面をスクロールさせるとビューホルダーのデータが更新され、それに合わせて実際に表示される項目(アイテム)も更新されます
  • ビューホルダーはアダプタの中で作成します
  • 今回は ItemAdapter クラス(アダプタ)内に、ItemViewHolder という名前のクラス(ビューホルダー)を作成します

クラス内に定義されたクラスは「ネストされたクラス(nested class)」っていいます

class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

    class ItemViewHolder()
}
  • ItemViewHolder クラス(ビューホルダー)は ItemAdapterクラス(アダプタ) の中だけで使われます(クラス内に作成するので確実にそうなります)
  • ビューホルダーをアダプタの中に記述しなくてもプログラムは動作しますが、こうした方がプログラムの構造が理解しやすくなります

ItemViewHolder クラスを記述する

ビューホルダーの中身を書くんですね

うん、ビューホルダーは RecyclerView.ViewHolder クラスを元にして作成するの

ItemViewHolder クラス(ビューホルダー)は、抽象クラスとして用意されている RecyclerView.ViewHolder のサブクラスとして作成します

class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }
}
  • ItemViewHolder クラスのコンストラクタには変数 view を設定します
  • view をスーパークラスのコンストラクタに渡します
  • ItemViewHolder クラスの中で、表示に使う TextView(UI部品)のインスタンスを textView プロパティで参照できるようにします
    • TextVeiw(UI部品)のリソースIDは item_title です(list_item.xml で定義したもの)
    • Kotlinコードからは R.id.item_title として利用できます

ビューホルダーができました

3つのメソッドを作成する

必ず作らないといけないメソッドが3つあるの

ItemAdapter クラスを RecyclerView.Adapter クラスのサブクラスにする

アダプタ は RecyclerView.Adapter クラスを元にするので、最初にをそれを記述します

ItemAdapter クラスを、抽象クラスとして用意されている RecyclerView.Adapter を継承するように修正します

class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }
}

あっ、なんかエラーがでちゃったかも…

うん、それは今から直すので大丈夫

  • RecyclerView.Adapter クラスにはオーバーライドが必要な抽象メソッドが3つ宣言されています
  • RecyclerView.Adapter クラスを継承するように修正すると、オーバーライドが必要なメソッドの記述がまだできていないのでエラーが出ます

オーバーライドするメソッドのひな型を作成する

オーバーライドが必要なメソッドのひな型は自動で作成できるの

  1. ItemAdapter の上にカーソルをおいて Control + I と入力します
  2. オーバーライドして記述しないといけないメソッドのリストが出てきます(以下の3つが出てきます)
    • getItemCount()
    • onCreateViewHolder()
    • onBindViewHolder()
  3. この3つを選択して OK ボタンを押します
  4. 選択した3つのメソッドのひな型(スタブ:stub)が自動的に作成されます

こんな感じになります

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    TODO("Not yet implemented")
}

override fun getItemCount(): Int {
    TODO("Not yet implemented")
}

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    TODO("Not yet implemented")
}

すごい、エラーが消えました!

  • メソッドが定義されたのでエラーは出なくなります
  • この3つのひな型に実際の処理を記述してアプリがちゃんと動作するようにします

getItemCount() を実装する

データソースの中のデータ数を入れておけばいいみたい

  • getItemCount() メソッドは、データソース(リスト)のサイズ(要素数)を返すように記述します
  • このアプリではデータソースは dataset プロパティに代入され、ItemAdapter クラスのコンストラクタに渡されています
  • dataset はリスト型です。dataset.size とすればサイズ(要素数)が分かります
  • 具体的にはこんな風に実装します
override fun getItemCount() = dataset.size

あれこれ省略せずに、こんな風に書いても構いません

override fun getItemCount(): Int {
    return dataset.size
}

onCreateViewHolder() を実装する

ビューホルダーのインスタンスを返すように記述すればいいの

  • ビューホルダーを新規に作成するためのメソッドです
  • このメソッドはレイアウトマネージャが利用します
  • 引数は2つです
  • 戻り値は新規に作成したビューホルダーのインスタンスです(ItemViewHolder型)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    // create a new view
    val adapterLayout = LayoutInflater.from(parent.context)
        .inflate(R.layout.list_item, parent, false)

    return ItemViewHolder(adapterLayout)
}
  • parent パラメータは、新規に追加される View の親になる View Group このと、つまり RecyclerView のことです
  • viewType パラメータは、今回はそのままにしておきます
    • ビューホルダーを表示するレイアウトが複数ある場合に設定が必要です
  • LayoutInflater は、RecyclerView(parent)の持っている情報(context)を使ってインスタンスを生成します
    • inflate() メソッドは XML レイアウトと親になる ViewGroup(RecyclerView)からUI部品(View)を生成します
    • 3つめの false に設定されている引数は attachToRoot です。false にしておくと RecyclerView の判断でちょうどいいときにView階層に追加されます
  • 最後に return 文でレイアウトを設定したビューホルダーのインスタンスを返すようにしておきます

onBindViewHolder() を実装する

ビューホルダーで管理しているUI部品の表示をデータにあわせて書き換えます

  • ビューホルダーの内容を書き換えるためのメソッドです
  • このメソッドはレイアウトマネージャが利用します
  • 引数は2つです
  • こんな感じになります
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    val item = dataset[position]
    holder.textView.text =  context.resources.getString(item.stringResourceId)
}
  • holder・・・ビューホルダー(ItemViewHolder型)
  • position・・・データソース(リスト)での現在の表示データ位置
  • item・・・データソース(リスト)の中の position の位置にあるインスタンス(データ)
  • stringResourceId・・・Affirmation クラス(データオブジェクト)のプロパティ(テキストメッセージのリソースIDを保持している)

item にデータのオブジェクトをいれて…

val item = dataset[position]

データソースのリストの指定位置に保管されているインスタンス(Affirmation型)を取得しています

TextView にテキストメッセージをセットします

holder.textView.text = context.resources.getString(item.stringResourceId)
  • ビューホルダーの TextView の内容を書き換えています
  • Affirmationインスタンスが保持しているテキストメッセージのリソースIDを使って strings.xml にある文字列を取得しています
    • AndroidフレームワークではリソースIDを引数にして getString() を使うと、リソースIDに対応した文字列が返されます
    • getString() は Resources クラスのメソッドです。Resources クラスのインスタンスは context として利用可能です
    • context.resources.getString(リソースID) とします
  • ビューホルダー(ItemViewHolderクラス)には textView としてTextView(UI部品)が参照できるように記述してあり、これを利用している

3つのメソッド全部、処理が記述できました

アダプタのKotlinコード(ItemAdapter.kt)のまとめ

完成したアダプタのコードはこんな感じかな?

package com.example.affirmations.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

/**
 * Adapter for the [RecyclerView] in [MainActivity]. Displays [Affirmation] data object.
 */
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    // Each data item is just an Affirmation object.
    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }

    /**
     * Create new views (invoked by the layout manager)
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        // create a new view
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)

        return ItemViewHolder(adapterLayout)
    }

    /**
     * Replace the contents of a view (invoked by the layout manager)
     */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.textView.text = context.resources.getString(item.stringResourceId)
    }

    /**
     * Return the size of your dataset (invoked by the layout manager)
     */
    override fun getItemCount() = dataset.size
}

アダプタ(ItemAdapterクラス)が完成しました。最後に RecyclerView にこのアダプタを設定してみます

RecyclerView を使うように MainActivity.kt を書き換える

作成したデータソースとアダプタを使って RecyclerView の設定をしますね

うーん、たのしみかも

  • UI画面で RecyclerView を使うには、MainActivity.kt にデータソース(Datasourceクラス)と アダプタ(ItemAdapterクラス)を設定します
  • MainActivity.kt を開いて次のように修正します
package com.example.affirmations

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.data.Datasource

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize data.
        val myDataset = Datasource().loadAffirmations()

        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.adapter = ItemAdapter(this, myDataset)

        // Use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        recyclerView.setHasFixedSize(true)
    }
}

データソースを読み込んで…

  • Datasource クラスのインスタンスを生成して loadAffirmations() メソッドを呼ぶとデータソースが返されます
  • 変数 myDataset に代入して利用します
val myDataset = Datasource().loadAffirmations()

RecyclerView を参照できるようにして…

  • レイアウトに配置した RecyclerView を参照できるようにします
  • findViewById() の戻り値を変数 recyclerView に代入しておきます
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)

RecyclerView にアダプタをとりつけて…

  • RecyclerView にアダプタ(ItemAdapterクラス)を設定します。RecyclerView 型を指定しておきます
  • ItemAdapter には2つのパラメータが必要です
    • このアクティビティをあらわす this
    • データソース(myDataset)
  • recyclerView の adapter プロパティに ItemAdapter オブジェクトを割り当てます
recyclerView.adapter = ItemAdapter(this, myDataset)

ちょっとおまけの設定もして…

  • RecyclerView のレイアウトサイズはアクティビティのレイアウトで変更されるので、recyclerView の setHasFixedSize を true にセットしておきます
  • この設定はパフォーマンス向上のためだけに必要です。必須ではありません
  • コンテンツの内容が変わっても RecyclerView のレイアウトサイズが変わらない場合に有効です
recyclerView.setHasFixedSize(true)

完成ですね!

アプリが完成しました

実行してみるね

うん、ちゃんとうごくかな?

  • アプリをビルドして実行してみます
  • strings.xml に設定した文字列が画面に表示されれば成功です

最後にコード全体を見渡しておきます

  • データソースとアダプタを用意して、アプリで RecyclerView を利用することができました
  • コードを見渡してそれぞれの部分がどんな風に協調して動いているのか考えてみましょう
  • 次回はこのコードを改善してアプリに画像を追加します。UIも、今より見栄えの良いかんじになおしてみます

おつかれさまでした!