ListAdapter no actualiza el elemento en RecyclerView

89

Estoy usando la nueva biblioteca de soporte ListAdapter. Aquí está mi código para el adaptador

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)
        }
    }
}

Estoy actualizando el adaptador con

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

Mi clase diferencial

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
    }
}

Lo que sucede es cuando se llama a submitList la primera vez que el adaptador procesa todos los elementos, pero cuando se llama de nuevo a submitList con las propiedades del objeto actualizadas, no se vuelve a representar la vista que ha cambiado.

Vuelve a renderizar la vista a medida que me desplazo por la lista, que a su vez llama bindView()

Además, he notado que llamar adapter.notifyDatasSetChanged()después de enviar la lista muestra la vista con valores actualizados, pero no quiero llamar notifyDataSetChanged()porque el adaptador de lista tiene utilidades de diferencia incorporadas

¿Me puede ayudar alguien?

Veeresh Charantimath
fuente
El problema puede estar relacionado ArtistsDiffy, por tanto, con su Artistpropia implementación .
tynn
Sí, yo también pienso lo mismo, pero parece que no puedo precisarlo
Veeresh Charantimath
Puede depurarlo o agregar declaraciones de registro. También puede agregar el código relevante a la pregunta.
tynn
también verifique esta pregunta, la resolví de manera diferente stackoverflow.com/questions/58232606/…
MisterCat

Respuestas:

100

Editar: Entiendo por qué sucede esto, ese no era mi punto. Mi punto es que al menos necesita dar una advertencia o llamar a la notifyDataSetChanged()función. Porque aparentemente estoy llamando a la submitList(...)función por una razón. Estoy bastante seguro de que la gente está tratando de averiguar qué salió mal durante horas hasta que descubren que submitList () ignora en silencio la llamada.

Esto se debe a Googleuna lógica extraña. Entonces, si pasa la misma lista al adaptador, ni siquiera llama al DiffUtil.

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

Realmente no entiendo el punto de esto ListAdaptersi no puede manejar cambios en la misma lista. Si desea cambiar los elementos de la lista, pasa a la ListAdaptery ve los cambios, entonces debe crear una copia profunda de la lista o debe usar regular RecyclerViewcon su propia DiffUtillclase.

insa_c
fuente
5
Porque requiere el estado anterior para realizar la diferencia. Por supuesto, no puede manejarlo si sobrescribe el estado anterior. O_o
EpicPandaForce
29
Sí, pero en ese momento, hay una razón por la que llamo submitList, ¿verdad? Al menos debería llamar al en notifyDataSetChanged()lugar de ignorar silenciosamente la llamada. Estoy bastante seguro de que la gente está tratando de averiguar qué salió mal durante horas hasta que se dan cuenta de que submitList()ignora en silencio la llamada.
insa_c
5
Así que volví a RecyclerView.Adapter<VH>y notifyDataSetChanged(). La vida es buena ahora. Buena cantidad de horas
perdidas
@insa_c Puede añadir 3 horas para el recuento, que de lo mucho que perdí tratando de entender por qué mi vista de lista no se actualiza en algunos casos extremos ...
Bencri
notifyDataSetChanged()es caro y anularía por completo el punto de tener una implementación basada en DiffUtil. Puede ser cuidadoso e intencionado al llamar submitListsolo con datos nuevos, pero en realidad eso es solo una trampa de rendimiento.
David Liu
62

La biblioteca asume que está usando Room o cualquier otro ORM que ofrece una nueva lista asíncrona cada vez que se actualiza, por lo que simplemente llamar a submitList funcionará, y para los desarrolladores descuidados, evita hacer los cálculos dos veces si se llama a la misma lista.

La respuesta aceptada es correcta, ofrece la explicación pero no la solución.

Lo que puede hacer en caso de que no esté usando ninguna de estas bibliotecas es:

submitList(null);
submitList(myList);

Otra solución sería anular submitList (que no causa ese parpadeo rápido) como tal:

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

O con el código de Kotlin:

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

Lógica cuestionable pero funciona perfectamente. Mi método preferido es el segundo porque no hace que cada fila obtenga una llamada onBind.

RJFares
fuente
4
Eso es un truco. Simplemente pase una copia de la lista. .submitList(new ArrayList(list))
Paul Woitaschek
2
He pasado la última hora tratando de averiguar cuál es el problema con mi lógica. Qué lógica tan extraña.
Jerry Oka para el
7
@PaulWoitaschek Esto no es un truco, está usando JAVA :) se usa para solucionar muchos problemas en bibliotecas donde el desarrollador está "durmiendo". La razón por la que elegiría esto en lugar de pasar .submitList (new ArrayList (lista)) es porque puede enviar listas en varios lugares de su código. Es posible que olvide crear una nueva matriz cada vez, es por eso que anula.
RJFares
1
@ Po10cio Es extraño principalmente porque cuando lo escribieron así, se asumió que solo se usaría con bibliotecas ORM que ofrecen nuevas listas cada vez. Si está pasando la misma lista pero actualizada, debe evitar eso, y esa sería la mejor manera
RJFares
1
Incluso con el uso de Room me encuentro con un problema similar.
Bink
21

con Kotlin solo necesita convertir su lista a una nueva MutableList como esta u otro tipo de lista según su uso

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Mina Samir
fuente
Eso es extraño, pero convertir la lista a mutableList funciona para mí. ¡Gracias!
Thanh-Nhon Nguyen
3
¿Por qué diablos funciona esto? Funciona pero es muy curioso por qué sucede esto.
marzo
en mi opinión, el ListAdapter no debe tener en cuenta la referencia de su lista, así que, ¿con eso? .toMutableList () envía una nueva lista de instancias al adaptador. Espero que eso sea lo suficientemente claro para ti. @ March3April4
Mina Samir
Gracias. Según su comentario, supuse que ListAdapter recibe su conjunto de datos como una forma de List <T>, que puede ser una lista mutable, o incluso una lista inmutable. Si paso una lista inmutable, los cambios que hice están bloqueados por el conjunto de datos en sí, no por el ListAdapter.
marzo 3
Creo que lo obtuviste @ March3April4 Además, preocúpate por el mecanismo que usas con los diff utils porque también tiene responsabilidades, calculará los elementos de la lista que deben cambiar o no;)
Mina Samir
9

Tuve un problema similar, pero la representación incorrecta fue causada por una combinación de setHasFixedSize(true)y android:layout_height="wrap_content". Por primera vez, el adaptador se suministró con una lista vacía, por lo que la altura nunca se actualizó y fue 0. De todos modos, esto resolvió mi problema. Alguien más puede tener el mismo problema y pensar que es un problema en el adaptador.

Jan Veselý
fuente
1
Sí, configure la vista de reciclaje en wrap_content actualizará la lista; si la configura en match_parent, no llamará al adaptador
Exel Staderlin
5

Si encuentra algunos problemas al usar

recycler_view.setHasFixedSize(true)

definitivamente deberías revisar este comentario: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

Resolvió el problema de mi lado.

(Aquí hay una captura de pantalla del comentario solicitado)

ingrese la descripción de la imagen aquí

Yoann.G
fuente
Un enlace a una solución es bienvenido, pero asegúrese de que su respuesta sea útil sin él: agregue contexto alrededor del enlace para que sus compañeros usuarios tengan una idea de qué es y por qué está allí, luego cite la parte más relevante de la página. está enlazando en caso de que la página de destino no esté disponible.
Mostafa Arian Nejad
4

Hoy también me topé con este "problema". Con la ayuda de la respuesta de insa_c y la solución de RJFares, hice una función de extensión de 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)
}

que luego se puede usar en cualquier lugar, por ejemplo:

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

y solo (y siempre) copia la lista si es la misma que la cargada actualmente.

Bojan P.
fuente
3

Según los documentos oficiales :

Siempre que llama a submitList , envía una nueva lista para que se diferencie y se muestre.

Esta es la razón por la que cada vez que llama a submitList en la lista anterior (ya enviada), no calcula la diferencia y no notifica al adaptador el cambio en el conjunto de datos.

Ashu Tyagi
fuente
2

Para mí, este problema apareció si estaba usando RecyclerViewdentro de ScrollViewcon nestedScrollingEnabled="false"y la altura de RV configurada en wrap_content.
El adaptador se actualizó correctamente y se llamó a la función de vinculación, pero los elementos no se mostraron; RecyclerViewse atascó en su tamaño original.

Cambiar ScrollViewpara NestedScrollViewsolucionar el problema.

Tomislav
fuente
2

En mi caso, olvidé configurar el LayoutManagerpara RecyclerView. El efecto de eso es el mismo que el descrito anteriormente.

just_user
fuente
1

Para cualquiera que tenga el mismo escenario que el mío, dejo mi solución, que no sé por qué está funcionando, aquí.

La solución que funcionó para mí fue de @Mina Samir, que envía la lista como una lista mutable.

Mi escenario de problema:

-Cargar una lista de amigos dentro de un fragmento.

  1. ActivityMain adjunta FragmentFriendList (observa los datos en vivo de los elementos de la base de datos de amigos) y, al mismo tiempo, solicita una solicitud http al servidor para obtener toda mi lista de amigos.

  2. Actualice o inserte los elementos del servidor http.

  3. Cada cambio enciende la devolución de llamada onChanged de los datos en vivo. Pero, cuando es la primera vez que abro la aplicación, lo que significa que no había nada en mi mesa, el submitList tiene éxito sin ningún error de ningún tipo, pero no aparece nada en la pantalla.

  4. Sin embargo, cuando es la segunda vez que abro la aplicación, los datos se cargan en la pantalla.

La solución es, como se mencionó anteriormente, enviar la lista como mutableList.

Marzo 3 abril 4
fuente
1

Tuve un problema similar. El problema estaba en las Difffunciones, que no comparaban adecuadamente los elementos. Cualquiera que tenga este problema, asegúrese de que sus Difffunciones (y por extensión sus clases de objeto de datos) contengan definiciones de comparación adecuadas, es decir, comparar todos los campos que podrían actualizarse en el nuevo elemento. Por ejemplo en la publicación original

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

Esta función (potencialmente) no hace lo que dice en la etiqueta: no compara el contenido de los dos elementos, a menos que haya anulado la equals()función en la Artistclase. En mi caso no lo había hecho, y la definición de areContentsTheSamesolo marcó uno de los campos necesarios, debido a mi descuido al implementarlo. Esto es igualdad estructural versus igualdad referencial, puedes encontrar más sobre esto aquí.

ampalmer
fuente
0

Necesitaba modificar mis DiffUtils

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

Para devolver realmente si el contenido es nuevo, no solo compare la identificación del modelo.

tonificantes
fuente
0

El uso de la primera respuesta de @RJFares actualiza la lista correctamente, pero no mantiene el estado de desplazamiento. Todo RecyclerViewcomienza desde la posición 0. Como solución alternativa, esto es lo que hice:

   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

   }

De esta manera, puedo mantener el estado de desplazamiento RecyclerViewjunto con los datos modificados.

iCantC
fuente