ListAdapter tidak memperbarui item di RecyclerView

92

Saya menggunakan pustaka dukungan baru ListAdapter. Ini kode saya untuk adaptor

class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(parent.inflate(R.layout.item_artist))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        fun bind(artist: Artist) {
            itemView.artistDetails.text = artist.artistAlbums
                    .plus(" Albums")
                    .plus(" \u2022 ")
                    .plus(artist.artistTracks)
                    .plus(" Tracks")
            itemView.artistName.text = artist.artistCover
            itemView.artistCoverImage.loadURL(artist.artistCover)
        }
    }
}

Saya memperbarui adaptor dengan

musicViewModel.getAllArtists().observe(this, Observer {
            it?.let {
                artistAdapter.submitList(it)
            }
        })

Kelas diff saya

class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
    override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
        return oldItem?.artistId == newItem?.artistId
    }

    override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
        return oldItem == newItem
    }
}

Apa yang terjadi adalah ketika submitList dipanggil pertama kali adaptor merender semua item, tetapi ketika submitList dipanggil lagi dengan properti objek yang diperbarui itu tidak merender ulang tampilan yang telah berubah.

Ini membuat ulang tampilan saat saya menggulir daftar, yang pada gilirannya memanggil bindView()

Juga, saya perhatikan bahwa memanggil adapter.notifyDatasSetChanged()setelah mengirimkan daftar merender tampilan dengan nilai yang diperbarui, tetapi saya tidak ingin memanggil notifyDataSetChanged()karena adaptor daftar memiliki utilitas diff built-in

Adakah yang bisa membantu saya di sini?

Veeresh Charantimath
sumber
Masalahnya mungkin terkait ArtistsDiffdan dengan demikian implementasi Artistitu sendiri.
tynn
Ya, saya juga berpikir sama, tapi sepertinya saya tidak bisa menunjukkannya
Veeresh Charantimath
Anda dapat men-debugnya atau menambahkan pernyataan log. Anda juga dapat menambahkan kode yang relevan ke pertanyaan tersebut.
tynn
periksa juga pertanyaan ini, saya menyelesaikannya secara berbeda stackoverflow.com/questions/58232606/…
MisterCat

Jawaban:

107

Sunting: Saya mengerti mengapa ini terjadi bukan itu maksud saya. Maksud saya adalah setidaknya perlu memberi peringatan atau memanggil notifyDataSetChanged()fungsi. Karena ternyata saya memanggilsubmitList(...) fungsi tersebut karena suatu alasan. Saya cukup yakin orang mencoba mencari tahu apa yang salah selama berjam-jam sampai mereka mengetahui submitList () mengabaikan panggilan secara diam-diam.

Ini karena Googlelogika yang aneh. Jadi jika Anda meneruskan daftar yang sama ke adaptor itu bahkan tidak memanggil DiffUtil.

public void submitList(final List<T> newList) {
    if (newList == mList) {
        // nothing to do
        return;
    }
....
}

Saya benar-benar tidak memahami keseluruhan poin ini ListAdapterjika tidak dapat menangani perubahan pada daftar yang sama. Jika Anda ingin mengubah item pada daftar yang Anda teruskan ke ListAdapterdan melihat perubahannya, Anda perlu membuat salinan dalam dari daftar atau Anda perlu menggunakan regular RecyclerViewdengan DiffUtillkelas Anda sendiri .

insa_c
sumber
5
Karena membutuhkan status sebelumnya untuk melakukan diff. Tentu saja itu tidak dapat menanganinya jika Anda menimpa keadaan sebelumnya. O_o
EpicPandaForce
34
Ya, tetapi pada saat itu, ada alasan mengapa saya menelepon submitList, bukan? Setidaknya harus memanggil notifyDataSetChanged()alih - alih mengabaikan panggilan secara diam-diam. Saya cukup yakin orang-orang mencoba untuk mencari tahu apa yang salah selama berjam-jam sampai mereka mengetahui submitList()panggilan mengabaikan diam-diam.
insa_c
6
Jadi saya kembali ke RecyclerView.Adapter<VH>dan notifyDataSetChanged(). LIfe baik sekarang. Jumlah jam yang terbuang
Udayaditya Barua
1
@insa_c Anda dapat menambahkan 3 jam ke hitungan Anda, itulah yang saya sia-siakan untuk mencoba memahami mengapa tampilan daftar saya tidak diperbarui dalam beberapa kasus edge ...
Bencri
1
notifyDataSetChanged()mahal dan akan benar-benar mengalahkan tujuan memiliki implementasi berbasis DiffUtil. Anda mungkin berhati-hati dan berniat memanggil submitListhanya dengan data baru, tapi sebenarnya itu hanya jebakan kinerja.
David Liu
63

Pustaka mengasumsikan Anda menggunakan Room atau ORM lain yang menawarkan daftar asinkron baru setiap kali diperbarui, jadi hanya memanggil submitList di atasnya akan berfungsi, dan untuk pengembang yang ceroboh, itu mencegah melakukan penghitungan dua kali jika daftar yang sama dipanggil.

Jawaban yang diterima benar, menawarkan penjelasan tetapi bukan solusi.

Apa yang dapat Anda lakukan jika Anda tidak menggunakan perpustakaan semacam itu adalah:

submitList(null);
submitList(myList);

Solusi lain adalah mengganti submitList (yang tidak menyebabkan kedipan cepat itu) seperti:

@Override
public void submitList(final List<Author> list) {
    super.submitList(list != null ? new ArrayList<>(list) : null);
}

Atau dengan kode Kotlin:

override fun submitList(list: List<CatItem>?) {
    super.submitList(list?.let { ArrayList(it) })
}

Logika yang dipertanyakan tetapi bekerja dengan sempurna. Metode pilihan saya adalah yang kedua karena tidak menyebabkan setiap baris mendapatkan panggilan onBind.

RJFares
sumber
7
Itu hack. Berikan saja salinan daftar. .submitList(new ArrayList(list))
Paul Woitaschek
3
Saya telah menghabiskan satu jam terakhir mencoba mencari tahu apa masalahnya dengan logika saya. Logika yang aneh.
Jerry Okafor
7
@PaulWoitaschek Ini bukan hack, ini menggunakan JAVA :) ini digunakan untuk memperbaiki banyak masalah di perpustakaan tempat pengembang sedang "tidur". Alasan mengapa Anda memilih ini daripada meneruskan .submitList (new ArrayList (list)) adalah karena Anda dapat mengirimkan daftar di banyak tempat dalam kode Anda. Anda mungkin lupa membuat larik baru setiap saat, itulah mengapa Anda menimpa.
RJFares
2
Bahkan dengan menggunakan Room, saya mengalami masalah serupa.
Bink
2
Rupanya ini berfungsi saat memperbarui daftar di viewmodel dengan item baru, tetapi ketika saya memperbarui properti (boolean - isSelected) dari item dalam daftar, ini masih tidak akan berfungsi .. Idk kenapa tapi DiffUtil mengembalikan item lama dan baru yang sama seperti saya ' sudah diperiksa. Tahu di mana masalah itu mungkin terjadi?
Ralph
23

dengan Kotlin hanya Anda perlu mengonversi daftar Anda ke MutableList baru seperti ini atau jenis daftar lain sesuai dengan penggunaan Anda

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Mina Samir
sumber
Aneh, tetapi mengubah daftar menjadi mutableList berhasil untuk saya. Terima kasih!
Thanh-Nhon Nguyen
4
Kenapa ini berhasil? Ini berhasil tetapi sangat ingin tahu mengapa ini terjadi.
Maret
menurut pendapat saya, ListAdapter tidak boleh terkait dengan referensi daftar Anda, jadi dengan itu? .toMutableList () Anda mengirimkan daftar contoh baru ke adaptor. Saya harap itu cukup jelas untuk Anda. @ March3April4
Mina Samir
Terima kasih. Menurut komentar Anda, saya menduga bahwa ListAdapter menerima set data itu sebagai bentuk Daftar <T>, yang dapat berupa daftar yang dapat diubah, atau bahkan daftar yang tidak dapat diubah. Jika saya membagikan daftar yang tidak dapat diubah, perubahan yang saya buat diblokir oleh dataset itu sendiri, bukan oleh ListAdapter.
MaretApril4
Saya pikir Anda mendapatkannya @ March3April4 Selain itu, perhatikan mekanisme yang Anda gunakan dengan utilitas diff karena juga memiliki tanggung jawab akan menghitung item dalam daftar harus berubah atau tidak;)
Mina Samir
10

Saya memiliki masalah yang sama tetapi rendering yang salah disebabkan oleh kombinasi setHasFixedSize(true)dan android:layout_height="wrap_content". Untuk pertama kalinya adaptor diberikan dengan daftar kosong sehingga ketinggian tidak pernah diperbarui dan sebelumnya 0. Bagaimanapun, ini memperbaiki masalah saya. Orang lain mungkin memiliki masalah yang sama dan akan mengira itu masalah pada adaptor.

Jan Veselý
sumber
1
Ya, setel recycleview ke wrap_content akan memperbarui daftar, jika Anda menyetelnya ke match_parent tidak akan memanggil adaptor
Exel Staderlin
5

Hari ini saya juga menemukan "masalah" ini. Dengan bantuan jawaban insa_c dan solusi RJFares, saya menjadikan diri saya fungsi ekstensi Kotlin:

/**
 * Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
 *
 * Originally, [ListAdapter] will not update the view if the provided list is the same as
 * currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
 * could never work - the [ListAdapter] must have the previous list if items to compare new
 * ones to using provided diff callback.
 * However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
 * the view to be updated. This extension function handles this case by making a copy of the
 * list if the provided list is the same instance as currently loaded one.
 *
 * For more info see 'RJFares' and 'insa_c' answers on
 * /programming/49726385/listadapter-not-updating-item-in-reyclerview
 */
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
    // ListAdapter<>.submitList() contains (stripped):
    //  if (newList == mList) {
    //      // nothing to do
    //      return;
    //  }
    this.submitList(if (list == this.currentList) list.toList() else list)
}

yang kemudian dapat digunakan di mana saja, misalnya:

viewModel.foundDevices.observe(this, Observer {
    binding.recyclerViewDevices.adapter.updateList(it)
})

dan hanya (dan selalu) menyalin daftar jika sama dengan yang dimuat saat ini.

Bojan P.
sumber
5

Jika Anda mengalami beberapa masalah saat menggunakan

recycler_view.setHasFixedSize(true)

Anda harus memeriksa komentar ini dengan jelas: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

Itu memecahkan masalah di pihak saya.

(Ini tangkapan layar dari komentar seperti yang diminta)

masukkan deskripsi gambar di sini

Yoann.G
sumber
Tautan ke sebuah solusi diperbolehkan, tetapi harap pastikan jawaban Anda berguna tanpanya: tambahkan konteks di sekitar tautan sehingga sesama pengguna Anda akan tahu apa itu dan mengapa itu ada, lalu kutip bagian paling relevan dari halaman Anda ' menautkan ulang jika halaman target tidak tersedia.
Mostafa Arian Nejad
3

Menurut dokumen resmi :

Setiap kali Anda memanggil submitList itu mengirimkan daftar baru untuk di-diff dan ditampilkan.

Inilah sebabnya mengapa setiap kali Anda memanggil submitList pada sebelumnya (daftar yang sudah dikirimkan), itu tidak menghitung Diff dan tidak memberi tahu adaptor untuk perubahan dalam dataset.

Ashu Tyagi
sumber
3

Dalam kasus saya, saya lupa menyetel LayoutManageruntuk RecyclerView. Efeknya sama seperti yang dijelaskan di atas.

just_user
sumber
2

Bagi saya, masalah ini muncul jika saya menggunakan RecyclerViewbagian dalam ScrollViewdengan nestedScrollingEnabled="false"dan ketinggian RV disetel ke wrap_content.
Adaptor diperbarui dengan benar dan fungsi bind dipanggil, tetapi item tidak ditampilkan - fileRecyclerView macet di ukuran aslinya.

Mengubah ScrollViewuntuk NestedScrollViewmemperbaiki masalah.

Tomislav
sumber
2

Saya punya masalah serupa. Masalahnya ada pada Difffungsi, yang tidak cukup membandingkan item. Siapapun dengan masalah ini, pastikan Difffungsi Anda (dan dengan ekstensi kelas objek data Anda) berisi definisi perbandingan yang tepat - yaitu membandingkan semua bidang yang mungkin diperbarui dalam item baru. Misalnya di postingan aslinya

    override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
    return oldItem == newItem
}

Fungsi ini (berpotensi) tidak melakukan apa yang tertulis di label: fungsi ini tidak membandingkan konten dari dua item - kecuali Anda telah mengganti equals()fungsi di Artistkelas. Dalam kasus saya, saya belum melakukannya, dan definisi areContentsTheSamehanya memeriksa salah satu bidang yang diperlukan, karena pengawasan saya saat menerapkannya. Ini adalah persamaan struktural vs. persamaan referensial, Anda dapat menemukannya lebih lanjut di sini

ampalmer
sumber
1

Untuk siapa saja yang memiliki skenario yang sama dengan saya, saya meninggalkan solusi saya, yang saya tidak tahu mengapa itu berhasil, di sini.

Solusi yang berhasil untuk saya adalah dari @Mina Samir, yang mengirimkan daftar sebagai daftar yang bisa diubah.

Skenario Masalah Saya:

-Memuat daftar teman di dalam sebuah fragmen.

  1. ActivityMain melampirkan FragmentFriendList (Mengamati liveata item db teman) dan pada saat yang sama, meminta permintaan http ke server untuk mendapatkan semua daftar teman saya.

  2. Perbarui atau masukkan item dari server http.

  3. Setiap perubahan memicu callback onChanged dari Liveata. Tapi, ketika ini pertama kalinya saya meluncurkan aplikasi, yang berarti tidak ada apa-apa di tabel saya, submitList berhasil tanpa kesalahan apa pun, tetapi tidak ada yang muncul di layar.

  4. Namun, saat ini kedua kalinya saya meluncurkan aplikasi, data sedang dimuat ke layar.

Solusinya adalah, seperti yang disebutkan di atas, mengirimkan daftar sebagai mutableList.

3 April 4
sumber
1

Alasan ListAdapter .submitlist Anda tidak dipanggil adalah karena objek yang Anda perbarui masih menyimpan alamat yang sama di memori.

Ketika Anda memperbarui objek dengan katakanlah .setText itu mengubah nilai dalam objek aslinya.

Sehingga ketika Anda memeriksa apakah object.id == object2.id itu akan kembali sama karena keduanya memiliki referensi ke lokasi yang sama di memori.

Solusinya adalah membuat objek baru dengan data yang diperbarui dan memasukkannya ke dalam daftar Anda. Kemudian submitList akan dipanggil dan itu akan bekerja dengan benar

gamedev-il
sumber
0

Saya perlu memodifikasi DiffUtils saya

override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {

Untuk benar-benar mengembalikan apakah isinya baru, tidak hanya membandingkan id model.

tonisives
sumber
0

Menggunakan @RJFares jawaban pertama berhasil memperbarui daftar, tetapi tidak mempertahankan status gulir. Keseluruhan RecyclerViewdimulai dari posisi ke-0. Sebagai solusinya, inilah yang saya lakukan:

   fun updateDataList(newList:List<String>){ //new list from DB or Network

     val tempList = dataList.toMutableList() // dataList is the old list
     tempList.addAll(newList)
     listAdapter.submitList(tempList) // Recyclerview Adapter Instance
     dataList = tempList

   }

Dengan cara ini, saya dapat mempertahankan status gulir RecyclerViewbersama dengan data yang diubah.

iCantC
sumber