【Android】世界一わかりやすいRecyclerViewの実装
この記事は2019年12月当時の情報です。
2021年5月現在、RecyclerViewのVersionは1.2です。同様に動作します。
この記事はFUN Advent Calendar 2019の3日目の記事です。
昨日の記事は、【ガチ比較】登校ルートをチャリで往くでした。
前置き
Androidアプリ開発で、長ーいリストや逐次中身のデータ更新があるリストを扱いたいこと、結構あります。
そこでよく使われるのがRecyclerViewというものです。
この記事を書こうとしたきっかけとしては、大抵の人がRecyclerViewについて検索してたどり着く記事は大抵RecyclerViewを単体で扱ってくれていないというところからです。
RxJava....? DataBind....? 記事を読む人の学習コストをわざわざ上げていることがよくわかりません。分かっているなら話は別ですが。
ということで、今回はRecyclerViewをとりあえず動かせる最低限のコードをかんたんに解説していきます。プロジェクト全体はGitHubにあげていますので参考にしてください。
https://github.com/DaisenKudo/TheMostSimpleRecyclerView/tree/master
本題
外部ライブラリ
appディレクトリに入っている方のbuild.gradleのdependenciesの中に以下を追記してください。
implementation 'androidx.recyclerview:recyclerview:1.1.0'
レイアウト
- main_activity.xml : Activityのレイアウト
- main_fragment.xml : Fragmentのレイアウト
- part_item_model.xml : RecyclerViewに入れたいアイテムのレイアウト
特にコレといって言うことはないです。
別にRecyclerViewに入れるアイテムはConstraintLayoutを使わなくてもいいです。
findViewByIdしたときの型だけ注意してください。
<?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:id="@+id/container_main_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.ui.main.MainActivity" />
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/container_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?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"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="30dp">
<TextView
android:id="@+id/tv_item_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
コード
- MainActivity.kt : 今回はFragmentの入れ物に徹していただきます。
- MainFragment.kt : 今回はRecyclerViewの入れ物に徹していただきます。
- MainViewAdapter.kt : RecyclerViewのアレコレを管理してもらいます
- MainViewHolder.kt : RecyclerViewにいれるアイテム(一つ分)のガワを定義します。
- ItemModel.kt : RecyclerViewにいれるアイテム(一つ分)の中身を定義します。
コードで結構陥りやすいのが、「Unresolved reference: R」というエラーです。
import文内のio.github.qlain.themostsimplerecyclerview
を適宜
(プロジェクトのPackage Name
に読みかえてください。Android Studioなら自動でimportを書いてくれますが、たまーにやってくれないことがあるので。
Model
RecyclerView内に入れるデータをまとめておくためのクラスです。data classにしてしまってもいいかもしれません。
今回はStringだけを格納していますが、Bundleのように決められたものしか入れられないってわけではないので、柔軟なリストが作れます。
package io.github.qlain.themostsimplerecyclerview.model
class ItemModel {
var text: String = ""
}
MainViewHolder
RecyclerViewで使いたいパーツを定義しておきます。
RecyclerViewのアイテム1つごとにViewAdapterによってインスタンス化(アイテムごとにMainViewHolderが1対1で紐付けられる)されます。
package io.github.qlain.themostsimplerecyclerview.model
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.github.qlain.themostsimplerecyclerview.R
class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.tv_item_model)
}
MainViewAdapter
RecyclerViewのアレコレを管理します。
RecyclerView内のアイテム更新とかはここでやりましょう。
Fragmentからメソッドを呼び出す形でもいいと思います。
package io.github.qlain.themostsimplerecyclerview.view.ui.main
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.github.qlain.themostsimplerecyclerview.R
import io.github.qlain.themostsimplerecyclerview.model.ItemModel
import io.github.qlain.themostsimplerecyclerview.model.MainViewHolder
class MainViewAdapter(
private val list: List<ItemModel>,
private val listener: ListListener
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
interface ListListener {
fun onClickItem(tappedView: View, itemModel: ItemModel)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.part_item_model, parent, false)
return MainViewHolder(itemView)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.findViewById<TextView>(R.id.tv_item_model).text = list[position].text
holder.itemView.setOnClickListener {
listener.onClickItem(it, list[position])
}
}
override fun getItemCount(): Int = list.size
}
MainActivity
MainFragmentの表示だけです。
package io.github.qlain.themostsimplerecyclerview.view.ui.main
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.github.qlain.themostsimplerecyclerview.R
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().replace(
R.id.container_main_fragment,
MainFragment()
).commit()
}
}
MainFragment
定義したRecyclerViewを表示させます。
今回はgenerateItemList()で花丸ちゃん、おまんじゅうn個目だよというテキストを生成しています。
val recyclerAdapter = MainViewAdapter(generateItemList(), object : MainViewAdapter.ListListener {
の行のgenerateItemList()
を差し替えると動的にリストを変更することもできます。
ConstraintLayoutなのにLinearLayoutManager...????と思うかもしれませんが気にしないでください。大丈夫です。
ちなみに、onDestroyViewの処理を実行しないと、メモリリークの原因になります。(ActivityよりRecyclerAdapterが長生きしてしまう恐れがある)
package io.github.qlain.themostsimplerecyclerview.view.ui.main
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.qlain.themostsimplerecyclerview.R
import io.github.qlain.themostsimplerecyclerview.model.ItemModel
class MainFragment : Fragment(){
private var recyclerView: RecyclerView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this.recyclerView = view.findViewById(R.id.container_recycler_view)
this.recyclerView?.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
itemAnimator = DefaultItemAnimator()
adapter = MainViewAdapter(
generateItemList(),
object : MainViewAdapter.ListListener {
override fun onClickItem(tappedView: View, itemModel: ItemModel) {
this@MainFragment.onClickItem(tappedView, itemModel)
}
}
)
}
}
override fun onDestroyView() {
super.onDestroyView()
this.recyclerView?.adapter = null
this.recyclerView = null
}
//RecyclerViewの生成時に一度だけ動く
private fun generateItemList(): List<ItemModel> {
val itemList = mutableListOf<ItemModel>()
for (i in 0..100) {
val item: ItemModel = ItemModel().apply {
text = "花丸ちゃん、おまんじゅう${i}個目だよ"
}
itemList.add(item)
}
return itemList
}
//RecyclerView内のアイテムがクリックされたときに動く
private fun onClickItem(tappedView: View, itemModel: ItemModel) {
}
}
できるもの
ちょっと画質が良くないですが、このようなものができます。
まとめ
再掲ですが、今回作ったコードはここに上げておきます。
わからないことはIssueで聞いてください。
ViewHolderはViewAdapterのインナークラスにしてしまってもいいと思います。
MIT Licenseとはいっていますが、誰が書いても似たようなコードになると思うので、アレコレしても怒りません。なんなら写経でも
Advent Calender、明日はnao(ki)さんです。
Discussion
丁寧でためになる記事を公開してくださってありがとうございます!
参考にさせていただきました。
記事内のMainActivity.kt 14行目が
R.id.container_main_activity,
となっていますが、正しくは
R.id.container_main_fragment,
ではないでしょうか?
container_main_activityは定義されていませんし、GitHubのソースコードでは後者のようになっているようです。
コメントありがとうございます。
まさにご指摘の通りです。記事を修正します。