RecyclerViewの知識を深めるために簡単なToDoアプリを作ってみます。オンラインでデータを保持する回はこの記事を理解したらステップアップしてみてください。
完成の動き

追加したら下のRecycleViewに追加されて、RecycleViewのリストを選択すると、「本当に削除していいのか」というアラートが表示され、「はい」をクリックすると、リストから削除される簡単なToDoAppです。
MainActivityの画面作り
何か書いてと書かれているEditTextと追加ボタンのButton、リストが表示されるRecycleViewを作っていきます。
activity_main.xmlに以下のコードを追加してください。
<EditText
android:id="@+id/et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:ems="10"
android:inputType="textPersonName"
android:hint="何か書いて"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="+"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et"
app:layout_constraintStart_toStartOf="parent" />
特徴的なコードの説明を行います。
EditText
android:ems="10"
これは、EditTextの幅を指定しています。
半角数字1文字の倍の数(今回は20)がどの大きさのスマホサイズでも表示されるので、ここが基準になっていると判断できます。
layout_widthでwrap_contentとemsで整数の数値を指定してあげると、文字の幅に合わせてEditTextの大きさが変わってくれるみたいです。
android:hint="何か書いて"
これは、EditTextのうっすら表示されるテキストです。何を書いて欲しいか明示することでわかりやすいUIになります。
android:inputType="text"
今回はただのtextなので、textを入力しています。
このinputTypeを変えると、入力画面が変化します。例えば、phoneを入力すると、このような画面になります。

androidx.recyclerview.widget.RecyclerView
app:layout_constraintTop_toBottomOf="@+id/et"
RecycleViewのTopをEditTextのBottomに合わせて表示しています。
以上がMainActivityのレイアウトになります。
RecycleViewのレイアウトを作る

レイアウトなので、resフォルダ→右クリック→New→Layout Resource Fileで名前をone_layoutにします。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="TextView"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
one_layout.xmlファイルの完成コード
特徴的なコードの説明をします。
androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
wrap_contentにすることで、1行分match_parentで幅いっぱいに表示していたのを、1行分の高さにすることができます。
1行分の高さは、TextViewで決めることができます。
このone_layoutファイルを使い回して、MainActivityのRecycleViewで使っていくので、どのように使っていくのか記述してあげる変換器(Adapter)が必要になります。
厳密にいうと、one_layoutファイルのTextViewのインスタンスを使い回しているだけです。

Adapterを作る

AdapterはKotlinファイルになるので、MainActivity.ktと同じ階層に作ります。
MainActivityの上のpackageを右クリック→New→Kotlin Class/Fileを選択して名前をRecyclerAdapterにします。

Classを選択してください。
ここで、one_layoutのTextViewに表示したいデータをRecyclerAdapterで指定したいので、作ります。
表示したいデータを作る
表示したいデータと言っても、MainActivityのEditTextで受け取ったStringを格納するだけのものなので、実際に直接値を用意するのではなく、受け取るための箱をデータとして作ります。
箱ということは変数です。
Adapterを作る時と同じようにKotlinファイルなので、MainActivityの上のpackageを右クリック→New→Kotlin Class/Fileを選択して名前をToDoDataにします。

データを作るので、Data classを選択してください。
package com.example.todoappaddremove
//リストのテンプレートを作る
//入ってくるものを今回はText1つだからその分用意する
data class ToDoData(
val myToDo: String
)
記述する内容は、変数としてval myToDo: Stringを書きます。
これで、受け取ったものを参照して、RecyclerViewに表示していきます。
RecyclerAdapterにどのようにして変換するか記述する
package com.example.todoappaddremove
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
//今回は引数でリストを用意 クラス名(private val 変数名:ArrayList<データクラス>) or MutableList
//MutableListの方が少し複雑なリストになる どちらも変更可能なリスト
//ArrayListに追加する型をデータクラスにすることでStringでもIntでも対応ができるようになる
class RecyclerAdapter(private val todoList:ArrayList<ToDoData>): RecyclerView.Adapter<RecyclerAdapter.ViewHolderItem>() {
//ViewHolderのインナークラス
//RecyclerViewのinnerクラスだと明示する
//16) ,rAdapter: RecyclerAdapter//RecyclerAdapterを受け取る
inner class ViewHolderItem(v: View,rAdapter:RecyclerAdapter): RecyclerView.ViewHolder(v) {
val tvHolder: TextView = v.findViewById(R.id.tv)
//one_layoutの1行が押されたら
init {
v.setOnClickListener {
val position:Int = adapterPosition
val listPosition = todoList[position]
//アラートで警告文を表示
AlertDialog.Builder(v.context)
.setTitle("${listPosition.myToDo}を選択しています")
.setMessage("本当に削除しますか")
.setPositiveButton("はい"){ _, _ ->
//リストから消す
todoList.removeAt(position)
//表示を更新
//表示から消す
rAdapter.notifyItemRemoved(position)
}
.setNegativeButton("いいえ",null)
.show()
//トーストで表示
//Toast.makeText(v.context, "${listPosition.myToDo}", Toast.LENGTH_SHORT).show()
}
}
}
//1行だけのViewを生成
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderItem {
val itemXml = LayoutInflater.from(parent.context)
.inflate(R.layout.one_layout, parent, false)
return ViewHolderItem(itemXml,this) //RecyclerAdapter自身に受け渡す
}
//position番目のデータをxmlに表示するようにセット
override fun onBindViewHolder(holder: ViewHolderItem, position: Int) {
val currentItem = todoList[position] //何番目のリストですか
holder.tvHolder.text = currentItem.myToDo
}
//リストのサイズ
override fun getItemCount(): Int {
return todoList.size
}
}
もっと簡単なものを作ろうとすると、innerクラスを使わず、ViewHolderItemというKotlinファイルを作ってそこにone_layout.xmlのTextViewについてどんなものを表示するかのコードを書くのですが、ここでは、AdapterでもTextViewのtextを使うのでinnnerクラスで記述をしています。
要は、たくさんTextViewを参照するから、使いやすいように1つのファイルでまとめているということです。
class RecyclerAdapter(private val todoList:ArrayList<ToDoData>): RecyclerView.Adapter<RecyclerAdapter.ViewHolderItem>() {
}
このコードを説明します。
Adapterの書き方として、まず、RecyclerView.Adapterと、このクラスはAdapterなんだよと宣言します。
class RecycleAdapter: RecyclerView.Adapter {
}
そして、AdapterにはViewHolderの型指定が必要なので、
class RecycleAdapter: RecyclerView.Adapter<RecyclerAdapter.ViewHolderItem>() {
}
<RecyclerAdapter.ViewHolderItem>と型を指定します。
多分ViewHolderItemがRecyclerAdapterの中にないよとエラーになるので、ViewHolderItemをinnerクラスで作っていきます。
inner class ViewHolderItem(v: View,rAdapter:RecyclerAdapter): RecyclerView.ViewHolder(v) {
val tvHolder: TextView = v.findViewById(R.id.tv)
//one_layoutの1行が押されたら
init {
v.setOnClickListener {
val position: Int = adapterPosition
val listPosition = todoList[position]
//アラートで警告文を表示
AlertDialog.Builder(v.context)
.setTitle("${listPosition.myToDo}を選択しています")
.setMessage("本当に削除しますか")
.setPositiveButton("はい") { _, _ ->
//リストから消す
todoList.removeAt(position)
//表示を更新
//表示から消す
rAdapter.notifyItemRemoved(position)
}
.setNegativeButton("いいえ", null)
.show()
}
}
}
inner class ViewHolderItem(v: View,rAdapter:RecyclerAdapter): RecyclerView.ViewHolder(v) {
}
ViewHolderItemには、引数としてViewを持たせないといけないです。
なので、(v: View)としています。vはお好きな変数名で大丈夫です。
このインナークラスは: RecyclerView.ViewHolder(v)だよと宣言しています。
rAdapter:RecyclerAdapterはrAdapter.notifyItemRemoved(position)で使うので、引数として宣言しています。
要は、RecyclerAdapterの中にあるnotifyItemRemovedを使って表示されているリストの削除を行いたいから引数としてクラスに落とし込んでいるということですね。
このリスト削除の流れは詳しく説明します。
val tvHolder: TextView = v.findViewById(R.id.tv)
ViewHolderの役目は、TextViewのtextに何を処理するか決めるということでした。ですので、TextViewをここで紐づけてtextをいじれるようにします。
init {
v.setOnClickListener {
}
}
initで初期化表示という意味なので、ViewHolderが読み込まれた時に紐づけてくれたり処理を行ってくれます。
initの詳しい話は、また別記事でまとめます。
v.setOnClickListenerは、Viewがクリックされたときに処理してくれるコードです。
val position: Int = adapterPosition
val listPosition = todoList[position]
adapterPositionでアダプターの何番目がクリックされたかを読み取って、todoListの配列のposition番目の値をlistPositionで保持します。
AlertDialog.Builder(v.context)
アラートで表示をして、タイトルやメッセージを表示します。
“”の中に変数を入れたいときは、$をつけます。
はいといいえの処理は、
.setPositiveButton、.setNegativeButtonで行います。
名前から分かる通り、positiveなので、はい。nagativeなのでいいえと言ったところですね。
第一引数に表示したいテキスト、第二引数に処理したい内容を書きます。
setPositiveButtonは、変則的な書き方になっていますが、処理を書くときに、省略形でKotlinがおすすめしている書き方になります。
//リストから消す
todoList.removeAt(position)
//表示を更新
//表示から消す
rAdapter.notifyItemRemoved(position)
はいの内容に進んだ時の処理の説明は、positionで選択した場所を格納しているので、その場所に対応する配列の内容をremoveAt()で削除しています。
notifyItemRemovedでアダプター経由で表示しているレイアウトを削除します。
//1行だけのViewを生成
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderItem {
val itemXml = LayoutInflater.from(parent.context)
.inflate(R.layout.one_layout, parent, false)
return ViewHolderItem(itemXml,this) //RecyclerAdapter自身に受け渡す
}
//position番目のデータをxmlに表示するようにセット
override fun onBindViewHolder(holder: ViewHolderItem, position: Int) {
val currentItem = todoList[position] //何番目のリストですか
holder.tvHolder.text = currentItem.myToDo
}
//リストのサイズ
override fun getItemCount(): Int {
return todoList.size
}
この3つは、ViewHolderで必須でそれぞれコメント文で役割を示しています。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderItem {
val itemXml = LayoutInflater.from(parent.context)
.inflate(R.layout.one_layout, parent, false)
return ViewHolderItem(itemXml,this) //RecyclerAdapter自身に受け渡す
}
このコードの説明は今の段階では詳しく理解できていませんが、LayoutInflaterやinflateというものについてはこちらで説明しています。

要は、infalterを使って、viewの情報をとってきてRecyclerAdapterへ渡しているという認識でとりあえずはいいのではないでしょうか。
override fun onBindViewHolder(holder: ViewHolderItem, position: Int) {
val currentItem = todoList[position] //何番目のリストですか
holder.tvHolder.text = currentItem.myToDo
}
bindは繋ぐという意味を持っているので、何かを繋いでいると推測できるでしょう。
そのままで、todoListのposition番目のStringをcurrentItemに代入して、holderが持っているTextViewのtextに代入しています。
override fun getItemCount(): Int {
return todoList.size
}
これはそのままです。リストのサイズを返します。
ここで、なぜcount()ではないのか疑問に思う人がいるかもしれません。
結論、sizeでもcount()でもどちらでも同じ動作はするのですが、Kotlinでの決まりごとのような感じです。
最初にこの関数を作った人がsizeで書いていたために、getItemCount()ではsizeを使うのがセオリーみたいですね。
MainActivityにAdapterを紐付け
package com.example.todoappaddremove
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
//追加されたリストの受け皿
private var addList = ArrayList<ToDoData>() //空のリストの時は型指定
//RecyclerView
private lateinit var recyclerView: RecyclerView
//アダプターを用意 追加リストをセットしてアダプターに描画してもらう
private var recyclerAdapter = RecyclerAdapter(addList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//viewの取得、アダプターにセット、レイアウトにセット
recyclerView = findViewById(R.id.rv)
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
//追加ボタンを押したら
val btnAdd: Button = findViewById(R.id.btnAdd)
btnAdd.setOnClickListener {
val et :EditText = findViewById(R.id.et)
val data = ToDoData(et.text.toString()) //Stringにする
addList.add(data)
//画面の更新
recyclerAdapter.notifyItemInserted(addList.lastIndex) //リストの最後に挿入
et.text = null
}
}
}
完成コード
//追加されたリストの受け皿
private var addList = ArrayList<ToDoData>() //空のリストの時は型指定
//RecyclerView
private lateinit var recyclerView: RecyclerView
//アダプターを用意 追加リストをセットしてアダプターに描画してもらう
private var recyclerAdapter = RecyclerAdapter(addList)
MainActivityに追加ボタンがあるため、ここでAdapterと紐付けを行います。
空のaddListとlateinitのrecyclerViewを用意して、addListをセットしたアダプターを用意します。
lateinitとは?
言葉の通り、late=遅れる、init=初期化なので、初期化を遅らせてくれるコードです。
なぜ遅らせる必要があるかというと、Viewの場合初めて初期化されるのは、onCreated()内の今回でいえば、
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初めて初期化
recyclerView = findViewById(R.id.rv)
}
ここで紐付けが行われ初期化されます。
なので、いつも通り
var recyclerView: RecyclerView
としてしまうと、nullになってしまうためエラーになります。だから、初期化処理を遅らせて、変数に代入するということになります。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//viewの取得、アダプターにセット、レイアウトにセット
recyclerView = findViewById(R.id.rv)
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
//追加ボタンを押したら
val btnAdd: Button = findViewById(R.id.btnAdd)
btnAdd.setOnClickListener {
val et :EditText = findViewById(R.id.et)
val data = ToDoData(et.text.toString()) //Stringにする
addList.add(data)
//画面の更新
recyclerAdapter.notifyItemInserted(addList.lastIndex) //リストの最後に挿入
et.text = null
}
}
onCreate()内を見ていきます。
recyclerView.adapter = recyclerAdapter
onCreate()より上のプロパティで宣言したrecyclerAdapterをrecyclerViewのアダプターにセットしています。
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerViewにあるlayoutManagerでどのように、レイアウト(one_layout.xml)を配置するか決めます。
今回はthisにしているので縦表示でレイアウト(one_layout.xml)を表示します。
こちらで少し詳しく説明しています。

btnAdd.setOnClickListener {
val et :EditText = findViewById(R.id.et)
val data = ToDoData(et.text.toString()) //Stringにする
addList.add(data)
//画面の更新
recyclerAdapter.notifyItemInserted(addList.lastIndex) //リストの最後に挿入
et.text = null
}
setOnClickListenerでクリック処理を書きます。
EditTextに打ち込まれたtextをdataに代入します。
空のaddListにdataを代入して、recyclerAdapterをnotifyItemInsertedでレイアウトの更新を行います。
アダプターのどこに入れるかは、addList.lastIndexで最後に挿入しています。
最後にEditTextのtextをnullにして、空白になるようにしています。
実行して確認してみましょう。
以上で完成です!お疲れ様です!