お首が長いのよお首が長いのよ

チラシの裏よりお届けするソフトウェアエンジニアとして成長したい人のためのブログ

2019-01-05

RecyclerView Adapter 一つに対して複数のViewHolderを持つリストを作成する

Kotlin でAndroidアプリ開発をはじめました。 まず、LegacyとなったListViewの代わりにRecyclerViewというのが新しく用意されているとの事で触ってみました。 ちゃんとしたオブジェクト指向でコードを書くのはかなり久々な事と、 AdapterとHolder、レイアウトファイルの関連が掴めていなかったのもあり、 理解に至るまで苦しみました。 後日順を追ってKotlinによるAndroid開発のメモは残しておきますが、この記事は先に残しておこうと思います。

やりたいこと:Adapter一つに対して複数のViewHolderを持つRecyclerViewを作る

例えば、RecyclerViewのアイテム一覧の一番最後は「+」アイコンで 要素の追加が出来るようなActivityを用意します。

ポイントとなるのは以下の通りです。

onCreateViewHolder

  • ViewTypeで一番最後か、そうでないかを判断し、読み込むレイアウトファイルを使い分けます
  • 予め用意したViewHolderクラスのスーパークラスにあたる RecyclerView.ViewHolder を戻り値で返します。サブクラス(用意したViewHolderクラス)は返しません

onBindViewHolder

  • 引数のPositionから一番最後か、そうでないかを判断し、読み込むViewHolderを使い分けます
  • RecyclerView.ViewHolderを引数として待ち受け、読み込むViewHolderに従ってキャストします。 > この時厳密に読み込むViewHolderが一致していないとキャストが出来なかったり、ウィジェットの要素がNullになって落ちるので判断する際は注意する。

レイアウトファイル(xml file)

RecyclerViewを表示するアクティビティ用レイアウトファイル (activity_new_post.xml)

後述する[A]及び[B]を組み合わせたものを期待しています。

xml
1<?xml version="1.0" encoding="utf-8"?>
2<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3        xmlns:app="http://schemas.android.com/apk/res-auto"
4        xmlns:tools="http://schemas.android.com/tools"
5        android:layout_width="match_parent"
6        android:layout_height="match_parent" tools:layout_editor_absoluteY="81dp">
7
8    <android.support.v7.widget.RecyclerView
9            android:id="@+id/addRecyclerView"
10            android:layout_width="match_parent"
11            android:layout_height="545dp"
12            android:layout_marginTop="8dp"
13            app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
14            android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"/>
15    <Button
16            android:id="@+id/btnAdd"
17            android:text="Post"
18            android:layout_width="match_parent"
19            android:layout_height="wrap_content"
20            android:layout_marginBottom="8dp"
21            app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
22            app:layout_constraintTop_toBottomOf="@+id/addRecyclerView" app:layout_constraintStart_toStartOf="parent"
23            android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"/>
24</android.support.constraint.ConstraintLayout>
25

[A]RecyclerViewのアイテム一つを表すレイアウトファイル (layout_new_item_card.xml)

CardViewの中にEditTextとImageButtonをおいてみました。予め、専用の画像等は適切なフォルダに入れておいてください。 Image Buttonの使い方を調べておきましょう。

xml
1<?xml version="1.0" encoding="utf-8"?>
2<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3                                             xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
4                                               android:layout_height="wrap_content"
5                                               xmlns:app="http://schemas.android.com/apk/res-auto">
6
7        <android.support.v7.widget.CardView
8            android:id="@+id/android_layout"
9            android:layout_width="match_parent"
10            android:layout_height="wrap_content"
11            app:cardBackgroundColor="@android:color/white">
12
13            <android.support.constraint.ConstraintLayout
14                android:layout_width="match_parent"
15                android:layout_height="wrap_content">
16
17                <EditText
18                        android:id="@+id/edtFlowerName"
19                        android:layout_width="wrap_content"
20                        android:layout_height="wrap_content"
21                        android:inputType="textPersonName"
22                        android:hint="花名を入力"
23                        android:ems="20" />
24
25                <ImageButton
26                        android:id="@+id/ibtnCamera"
27                        android:layout_width="wrap_content"
28                        android:layout_height="wrap_content"
29                        app:srcCompat="@android:drawable/ic_menu_camera"
30                        app:layout_constraintTop_toBottomOf="@+id/edtFlowerName" />
31
32            </android.support.constraint.ConstraintLayout>
33        </android.support.v7.widget.CardView>
34</android.support.constraint.ConstraintLayout>
35

[B]RecyclerViewの一番最後の要素に表示するレイアウトファイル (layout_add_button.xml)

Image Buttonで+アイコンのみ配置しています

xml
1<?xml version="1.0" encoding="utf-8"?>
2<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3                                             xmlns:app="http://schemas.android.com/apk/res-auto"
4                                             xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
5                                             android:layout_height="wrap_content">
6
7    <ImageButton
8            android:id="@+id/ibtnItemAdd"
9            android:layout_width="wrap_content"
10            android:layout_height="wrap_content" app:srcCompat="@android:drawable/ic_menu_add"
11            app:layout_constraintEnd_toEndOf="parent"
12            android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
13            android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintTop_toTopOf="parent"
14            android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"/>
15</android.support.constraint.ConstraintLayout>
16

Holder

ViewHolder A (NewPostViewHolder.kt)

Java
1package com.killinsun.app.recyclepractice
2
3import android.support.v7.widget.RecyclerView
4import android.view.View
5import android.widget.EditText
6import android.widget.ImageButton
7
8class NewPostViewHolder(view: View) : RecyclerView.ViewHolder(view) {
9
10    // 最後の行以外の場合はこっちを読み込む。 
11    val edtFlowerName: EditText? = view.findViewById(R.id.edtFlowerName)
12    val ibtnCamera: ImageButton? = view.findViewById(R.id.ibtnCamera)
13
14}
15

ViewHolder B (NewItemViewHolder.kt)

Java
1package com.killinsun.app.recyclerpractice
2
3import android.support.v7.widget.RecyclerView
4import android.view.View
5import android.widget.ImageButton
6
7class NewItemViewHolder(view: View) : RecyclerView.ViewHolder(view){
8
9    // 最後の行だった場合はこっちを読み込む
10    val ibtnItemAdd: ImageButton = view.findViewById(R.id.ibtnItemAdd)
11}
12

Adapter(NewPostAdapter.kt)

Java
1package com.killinsun.app.recyclepractice
2
3import android.support.v7.widget.RecyclerView
4import android.util.Log
5import android.view.LayoutInflater
6import android.view.View
7import android.view.ViewGroup
8import android.widget.ArrayAdapter
9import kotlinx.android.synthetic.main.layout_new_item_card.view.*
10
11class NewPostAdapter(private val itemList:ArrayList<Int>)
12    : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
13
14    override fun getItemViewType(position: Int) : Int{
15       return if(position == itemList.size) 0 else 1  // ①
16    }
17
18    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
19
20        val layoutInflater = LayoutInflater.from(parent.context)
21        val view: View  // ②
22
23       // ③
24        if(viewType == 0){
25            //最後の行の場合は追加ボタンレイアウトを読み込む
26            view = layoutInflater.inflate(R.layout.layout_add_button, parent, false)
27            return NewItemViewHolder(view)
28        }else {
29            //最後の行以外の場合はそのままカードを読み込む
30            view = layoutInflater.inflate(R.layout.layout_new_item_card, parent, false)
31            return NewPostViewHolder(view)
32        }
33
34    }
35
36    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
37       // ④
38        if(position == itemList.size){
39            val myHolder: NewItemViewHolder = holder as NewItemViewHolder
40            myHolder.ibtnItemAdd.setOnClickListener{
41                Log.v("test","Add button clicked!!!")
42                itemList.add(position+1)
43                notifyItemInserted(position + 1)
44            }
45        }else {
46            val myHolder: NewPostViewHolder = holder as NewPostViewHolder
47            myHolder.ibtnCamera?.setOnClickListener {
48                Log.v("test", "Camera button clicked!!!")
49            }
50        }
51
52    }
53
54    override fun getItemCount(): Int{
55        return itemList.size + 1  // ⑤
56    }
57
58}
59

getItemViewメソッドでポジションを受け取り、用意したitemListとサイズが一致(=与えられた要素の位置が最終行)の場合のみ0を返却し、それ以外の行(=与えられた要素の位置が最終行以外)の場合は、1を返却します。

予め View型のオブジェクトを用意しておきます。

getItemViewTypeで得られた戻り値(0 or 1)を基に判断を進めます。 上述の通り0の場合は最終行なのでそれ用のレイアウトファイルをlayoutInflaterメソッドにより読み込み、②で宣言したViewオブジェクトに代入します。その後、メソッドとしてViewHolderクラスを戻り値として返して終了します。 この時返却するViewHolderクラスは、要素に応じたものです。 最終行以外の場合も、読み込むレイアウトファイルと戻り値のViewHolderクラスが違うだけで、やっていることは一緒です。

これまでと同様に、今回はpositionの値によって最終行かそうでないかを判断しています。 myHolderに対しては、最終行だったら引数として読み込まれた holderRecycleView.ViewHolder型)を NewItemViewHolder型にキャストといった処理をしています。最終行以外の場合も、相応の処理を行っています。 また、この時別々のクラス型にキャストしてあげる事で、ViewHolder内で宣言したウィジェット(例えば最終要素の場合は+ボタン)のsetOnClickListenerにアクセス出来るようになっています。

ちなみに、今回は+ボタンを押された際にRecyclerViewの要素が1つ増える処理が記載されています。

おまじないです。 このRecyclerViewは要素数+1は常に「+ボタン」になっています。

Activity

Java
1package com.killinsun.app.recyclerpractice
2
3import android.os.Bundle
4import android.support.v7.app.AppCompatActivity
5import android.support.v7.widget.LinearLayoutManager
6import kotlinx.android.synthetic.main.activity_new_post.*
7
8class NewPostActivity : AppCompatActivity(){
9
10    override fun onCreate(savedInstanceState: Bundle?){
11        super.onCreate(savedInstanceState)
12        setContentView(R.layout.activity_new_post)
13
14        var listItems: ArrayList<Int> = arrayListOf(0)
15
16        addRecyclerView.layoutManager = LinearLayoutManager(this)
17        addRecyclerView.adapter = NewPostAdapter(listItems)
18
19    }
20}
21

今回のアクティビティではユーザの操作によってRecyclerViewの要素が増える事を想定したものの為、Adapterに渡す配列はInt型で1要素のみを渡しています。

やってみて

ココ最近のAndroidアプリ開発は右も左も分からないまま突っ込んで、ようやくAdapterとHolderの仕組みが少し理解できたかなという気持ちです。 最初は戻り値となるViewHolderをサブクラス(NewItemViewHolder)に指定してたりして、「どうやってもう片方のViewHolderの受け取るんだよ」って躓いたりしました。スーパークラスにしておけばいいんですね・・・。

正直、上記の記述がベストプラクティスだとは思っていませんし、改善の余地があるかと思いますが、 ひとまず「動くもの」を目指して作るのは大事な事だと思っています。 その上でリファクタリングして更に理解を深められたらいいな。

いずれにしてもAdapterにあまり処理を書きたくないと思っているので、どこに移動させるのがベストか、 意見がありましたらツイッターでもコメントでもフィードバックいただけると嬉しいです。

参考

/以上

よかったらシェアしてください!