初投稿になります
初めまして、これが初投稿になります。
今年の四月にSEになり、そこからプログラミングを本格的に始めたばかりなのでまだまだ未熟ですが誰かの助けになる投稿ができればと思います。
また詳しい方はいろいろ間違ってる部分の指摘等教えていただければ幸いです。
記事概要
この記事では、RecyclerViewの更新がAdapter.notifyDataSetChanged()でできない理由についての私的な考察のまとめです。同じお悩みの方の解決の一助になる、はず。
私的というのは実際にRecyclerViewクラスのコードから仕様を読み取ったわけではなく、実行結果から個人的な推測を行ったというニュアンスです。コードまで詳しい方は考察が正しいのか等教えていただけると助かります。
またRecyclerViewに関する実装方法など知っているが、notifyDataSetChanged()で更新できなかった人向けに書いているので、RecyclerViewの実装方法にはあまり触れません。実装方法などわからない方は詳ししくは他の記事を調べてていただくといいと思います。公式はこちらになります。
また記載のコードは極力コピペで検証できるようにと思っておりますが、実行環境により動かない可能性もあります。ご了承ください。
最後に、かなり長くなってしまったので考察に移る前に結論を書いて置きます。
Adapter.notifyDataSetChanged()でできない理由は、Adapterが変更を認識するのは引数に入れられた変数自体ではなく、その変数の中身のリストだから、変数に別のリストを再代入すると変更が認識できないため。
それではお時間ある方は最後まで考察にお付き合いいただければと思います。
考察内容
notifyDataSetChanged()が正常に動く場合
そもそも更新がそれだけでできるのか?という話もあると思うので、先にnotifyDataSetChanged()で更新できる例から紹介します。
例としてボタンを押すとviewに数次が一つずつ増えていく簡単なアプリを作りました。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="368dp"
android:layout_height="439dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button" />
</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
data class ListData(var text : String)
class ViewHolder(view:View):RecyclerView.ViewHolder(view){
val text :TextView = view.findViewById(R.id.textView)
}
class ViewAdapter(val list :List<String>,val context :Context):RecyclerView.Adapter<ViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.list,parent,false)
val viewHolder = ViewHolder(view)
return viewHolder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.text.text = list[position]
}
override fun getItemCount(): Int {
return list.size
}
}
class MainActivity : AppCompatActivity() {
var sampleList = mutableListOf<String>("0")
var i = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val Adapter = ViewAdapter(sampleList,this)
val viewManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = Adapter
}
button.setOnClickListener{
sampleList.add(i.toString())
Adapter.notifyDataSetChanged()
i++
}
}
}
これでボタンを押せばRecyclerViewに数字が増えていくものができました。私の環境ではちゃんと更新されました。
更新されない一例
上記のコードで、MainActivity.ktを以下のように変えてしまうと更新されなくなってしまいます。
class MainActivity : AppCompatActivity() {
var sampleList = mutableListOf<String>("0") //(1)
var i = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val Adapter = ViewAdapter(sampleList,this) //(2)
val viewManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = Adapter //(3)
}
button.setOnClickListener{
sampleList = mutableListOf<String>() //(4)
for (j in 0..i){
sampleList.add(j.toString())
}
Adapter.notifyDataSetChanged() //(5)
i++
}
}
}
こんなあほみたいな実装せんわ!!と突っ込みたいのはわかりますが、でもnotifyDataSetChanged()で更新できない一例をミニマムに示そうとした結果です。一見私のような不束者には前例と同じように動きそうに見えるのですが、このようにしてしまうとボタンを押しても更新されません。
では何が問題なのか
問題となっているのはsampleListにmutableListOf()を再代入している所です。
それではなぜ問題なのか。それはAdapterにセットしたsampleListの中身のmutableListと再度代入したmutableListが違うものだからっぽいです。上のコメントアウトをもとに順を追って説明します。
(1)で代入されているmutableListをリストAとします。sampleListはただの箱で、その中身がリストAだという認識です。このリストAが(2)でAdapterにセットされ、そのAdapterが(3)でrecyclerViewに適応されています。つまりリストAがAdapter、ひいては recyclerViewに反映されています。
(4)でsampleListにmutableListを再代入していますが、これをリストBとします。このリストBに要素を加えているのがfor()の部分になります。
さて問題の(5)のところです。sampleListという箱の中身はリストBに変化していますが、AdapterにセットされているリストAには何の追加も変更もありません。よってAdapter.notifyDataSetChanged()を実行してもリストAの変化をrecyclerViewに反映するので、Viewになんの変化もないわけです。プレゼントを受け取った後、その箱に新しい中身が加えられようが知ったこっちゃないっていう話です。
これ、言われてみれば当たり前じゃんって話なんだと思いますが、私は最近までわからずに、毎回Adapterにも再代入して、recyclerviewにも再代入してっていうことをやっていました。
はまりポイント
この実装のはまりポイントはAdapterは引数にとった変数の変化をキャッチするのではないということです。valで基本的に書かれることを想定してのことだと思います。だからセオリー通りどうしてもというとき以外はvalを使って実装する方はこの問題に直面しないのかもしれません。そのような多くの方々にはなぜvarで再代入なんてしようと思ったんだと思われるかと思います。
今回の例で再代入しようとする人はいないかもしれませんが、リストの全部または部分的に入れ替える際には、すでに入れ替えて表示したいリストがある状態(ボタンを押すとあらかじめ用意していたリストと切り替えるとか)なら再代入したほうが行数も処理もわかりやすくなってやってしまうことがあるのではないでしょうか?
...え、そんな奴いない?私だけ?それは失礼しました。
結論と対策
要するにvalを基本的に使おうっていう方針を守りつつ、.removeAll{}とか.add()とか使ってリストを更新しようなってことですかね。
まとめ
初投稿最後までお読みいただきありがとうございました。読みずらく、単調に長いものになってしまったのではと反省してます。今後も何かあれば投稿いたしますので、どんどん役立つ投稿ができるように精進します。