📲

【Android】世界一わかりやすいRecyclerViewの実装

2021/05/22に公開
2

この記事は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の中に以下を追記してください。

build.gradle
implementation 'androidx.recyclerview:recyclerview:1.1.0'

レイアウト

  • main_activity.xml : Activityのレイアウト
  • main_fragment.xml : Fragmentのレイアウト
  • part_item_model.xml : RecyclerViewに入れたいアイテムのレイアウト

特にコレといって言うことはないです。
別にRecyclerViewに入れるアイテムはConstraintLayoutを使わなくてもいいです。
findViewByIdしたときの型だけ注意してください。

activity_main.xml
<?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" />
fragment_main.xml
<?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>
part_item_model
<?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のように決められたものしか入れられないってわけではないので、柔軟なリストが作れます。

ItemModel.kt
package io.github.qlain.themostsimplerecyclerview.model

class ItemModel {
    var text: String = ""
}
MainViewHolder

RecyclerViewで使いたいパーツを定義しておきます。
RecyclerViewのアイテム1つごとにViewAdapterによってインスタンス化(アイテムごとにMainViewHolderが1対1で紐付けられる)されます。

MainViewHolder.kt
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からメソッドを呼び出す形でもいいと思います。

MainViewAdapter.kt
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の表示だけです。

MainActivity.kt
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が長生きしてしまう恐れがある)

MainFragment.kt
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) {
    }
}

できるもの

ちょっと画質が良くないですが、このようなものができます。
demo.gif

まとめ

再掲ですが、今回作ったコードはここに上げておきます。
わからないことは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のソースコードでは後者のようになっているようです。

https://github.com/Squ4t4r014/TheMostSimpleRecyclerView/blob/master/app/src/main/java/io/github/qlain/themostsimplerecyclerview/view/ui/main/MainActivity.kt

Squ4t4r014Squ4t4r014

コメントありがとうございます。
まさにご指摘の通りです。記事を修正します。