ListAdapter não está atualizando o item no RecyclerView

92

Estou usando a nova biblioteca de suporte ListAdapter. Aqui está meu código para o 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)
        }
    }
}

Estou atualizando o adaptador com

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

Minha classe diff

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

O que está acontecendo é quando submitList é chamado na primeira vez que o adaptador renderiza todos os itens, mas quando submitList é chamado novamente com propriedades de objeto atualizadas, ele não renderiza novamente a visualização que foi alterada.

Ele renderiza novamente a visualização conforme eu rolar a lista, que por sua vez chama bindView()

Além disso, percebi que chamar adapter.notifyDatasSetChanged()após enviar lista renderiza a visualização com valores atualizados, mas não quero chamar notifyDataSetChanged()porque o adaptador de lista tem utilitários diff integrados

Alguém pode me ajudar aqui?

Veeresh Charantimath
fonte
O problema pode estar relacionado ArtistsDiffe, portanto, à implementação de Artistsi mesmo.
tynn
Sim, eu também penso o mesmo, mas não consigo definir
exatamente
Você pode depurá-lo ou adicionar instruções de log. Além disso, você pode adicionar o código relevante à questão.
tynn
também verifique esta questão, eu resolvi de forma diferente stackoverflow.com/questions/58232606/…
MisterCat

Respostas:

108

Edit: Eu entendo por que isso acontece, não era meu ponto. Meu ponto é que pelo menos precisa dar um aviso ou chamar a notifyDataSetChanged()função. Porque aparentemente estou chamando a submitList(...)função por um motivo. Tenho certeza de que as pessoas estão tentando descobrir o que deu errado por horas até descobrirem que submitList () ignora a chamada silenciosamente.

Isso ocorre por causa de Googleuma lógica estranha. Portanto, se você passar a mesma lista para o adaptador, ele nem chamará o DiffUtil.

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

Eu realmente não entendo todo o sentido disso, ListAdapterse ele não pode lidar com mudanças na mesma lista. Se você deseja alterar os itens da lista que você passa para o ListAdaptere ver as alterações, então você precisa criar uma cópia profunda da lista ou usar regular RecyclerViewcom sua própria DiffUtillclasse.

insa_c
fonte
5
Porque requer o estado anterior para realizar a comparação. É claro que isso não será possível se você substituir o estado anterior. O_o
EpicPandaForce
34
Sim, mas nesse ponto, há uma razão para eu ligar para o submitList, certo? Deve pelo menos chamar o em notifyDataSetChanged()vez de ignorar silenciosamente a chamada. Tenho certeza de que as pessoas estão tentando descobrir o que deu errado por horas até que eles descubram o submitList()ignora silenciosamente a ligação.
insa_c
6
Então, estou de volta ao RecyclerView.Adapter<VH>e notifyDataSetChanged(). A vida está boa agora. Boa quantidade de horas
perdidas
1
@insa_c Você pode adicionar 3 horas à sua contagem, é o quanto eu perdi tentando entender por que meu listview não estava atualizando em alguns casos
extremos
1
notifyDataSetChanged()é caro e derrotaria completamente o ponto de ter uma implementação baseada no DiffUtil. Você pode ser cuidadoso e atento ao chamar submitListapenas com novos dados, mas na verdade isso é apenas uma armadilha de desempenho.
David Liu,
63

A biblioteca assume que você está usando o Room ou qualquer outro ORM que oferece uma nova lista assíncrona toda vez que for atualizada, então, apenas chamar submitList nela funcionará e, para desenvolvedores desleixados, evita fazer os cálculos duas vezes se a mesma lista for chamada.

A resposta aceita é correta, oferece a explicação, mas não a solução.

O que você pode fazer caso não esteja usando nenhuma dessas bibliotecas é:

submitList(null);
submitList(myList);

Outra solução seria substituir submitList (o que não causa aquela piscada rápida) como tal:

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

Ou com código Kotlin:

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

Lógica questionável, mas funciona perfeitamente. Meu método preferido é o segundo porque ele não faz com que cada linha receba uma chamada onBind.

RJFares
fonte
7
Isso é um hack. Basta passar uma cópia da lista. .submitList(new ArrayList(list))
Paul Woitaschek de
3
Passei a última hora tentando descobrir qual é o problema com minha lógica. Uma lógica tão estranha.
Jerry Okafor
7
@PaulWoitaschek Este não é um hack, ele está usando JAVA :) é usado para consertar muitos problemas em bibliotecas onde o desenvolvedor está "dormindo". A razão pela qual você escolheria isso em vez de passar .submitList (new ArrayList (list)) é porque você pode enviar listas em vários lugares em seu código. Você pode esquecer de criar um novo array toda vez, é por isso que você o substitui.
RJFares
2
Mesmo usando o Room, estou tendo um problema semelhante.
Bink
2
Aparentemente, isso funciona ao atualizar uma lista em viewmodel com novos itens, mas quando eu atualizo uma propriedade (boolean - isSelected) de um item em uma lista, isso ainda não funcionará. Idk porque, mas DiffUtil retorna o mesmo item antigo e novo que eu ' eu verifiquei. Alguma ideia de onde o problema pode ocorrer?
Ralph de
23

com Kotlin você só precisa converter sua lista para uma nova MutableList como esta ou outro tipo de lista de acordo com seu uso

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Mina Samir
fonte
Isso é estranho, mas converter a lista em mutableList funciona para mim. Obrigado!
Thanh-Nhon Nguyen
4
Por que diabos isso está funcionando? Funciona, mas é muito curioso por que isso acontece.
março de 4 de
na minha opinião, o ListAdapter não deve se preocupar com a referência da sua lista, então? .toMutableList () você envia uma nova lista de instâncias ao adaptador. Espero que esteja claro o suficiente para você. @ March3April4
Mina Samir
Obrigado. De acordo com seu comentário, adivinhei que o ListAdapter recebe seu conjunto de dados como uma forma de List <T>, que pode ser uma lista mutável, ou mesmo uma lista imutável. Se eu distribuir uma lista imutável, as alterações que fiz estão sendo bloqueadas pelo próprio conjunto de dados, não pelo ListAdapter.
3Abril4
Eu acho que você entendeu @ March3April4 Além disso, se preocupe com o mecanismo que você usa com os utilitários diff porque ele também tem responsabilidades irá calcular os itens na lista devem mudar ou não;)
Mina Samir
10

Tive um problema semelhante, mas a renderização incorreta foi causada por uma combinação de setHasFixedSize(true)e android:layout_height="wrap_content". Pela primeira vez, o adaptador foi fornecido com uma lista vazia, de modo que a altura nunca foi atualizada e foi 0. Enfim, isso resolveu meu problema. Outra pessoa pode ter o mesmo problema e pensar que é um problema no adaptador.

Jan Veselý
fonte
1
Sim, definir o recycleview para wrap_content atualizará a lista, se você definir para match_parent ele não chamará o adaptador
Exel Staderlin
5

Hoje também me deparei com esse "problema". Com a ajuda da resposta do insa_c e da solução da RJFares, criei uma função de extensão do 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 pode ser usado em qualquer lugar, por exemplo:

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

e ele apenas (e sempre) copia a lista se for a mesma que está carregada no momento.

Bojan P.
fonte
5

Se você encontrar alguns problemas ao usar

recycler_view.setHasFixedSize(true)

você definitivamente deve verificar este comentário: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

Resolveu o problema do meu lado.

(Aqui está uma captura de tela do comentário, conforme solicitado)

insira a descrição da imagem aqui

Yoann.G
fonte
Um link para uma solução é bem-vindo, mas certifique-se de que sua resposta seja útil sem ele: adicione contexto ao link para que seus outros usuários tenham uma ideia do que ele é e por que está lá, depois cite a parte mais relevante da página que novo link para caso a página de destino não esteja disponível.
Mostafa Arian Nejad
3

De acordo com os documentos oficiais :

Sempre que você chama submitList, ele envia uma nova lista para ser analisada e exibida.

É por isso que sempre que você chama submitList na lista anterior (lista já enviada), ele não calcula o Diff e não notifica o adaptador sobre a mudança no conjunto de dados.

Ashu Tyagi
fonte
3

No meu caso, esqueci de definir o LayoutManagerpara o RecyclerView. O efeito disso é o mesmo descrito acima.

just_user
fonte
2

Para mim, esta questão parecia se eu estava usando RecyclerViewdentro de ScrollViewcom nestedScrollingEnabled="false"e conjunto de altura RV wrap_content.
O adaptador foi atualizado corretamente e a função de ligação foi chamada, mas os itens não foram mostrados - o RecyclerViewestava preso em seu 'tamanho original.

Mudar ScrollViewpara NestedScrollViewcorrigiu o problema.

Tomislav
fonte
2

Eu tive um problema parecido. O problema estava nas Difffunções, que não comparavam adequadamente os itens. Qualquer pessoa com esse problema, certifique-se de que suas Difffunções (e, por extensão, suas classes de objetos de dados) contenham definições de comparação adequadas - isto é, comparando todos os campos que podem ser atualizados no novo item. Por exemplo na postagem original

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

Esta função (potencialmente) não faz o que diz no rótulo: ela não compara o conteúdo dos dois itens - a menos que você tenha sobrescrito a equals()função na Artistclasse. No meu caso, não o fiz, e a definição de areContentsTheSameapenas verifiquei um dos campos necessários, devido ao meu descuido na implementação. Isso é igualdade estrutural vs. igualdade referencial, você pode encontrar mais sobre isso aqui

ampalmer
fonte
1

Para quem o cenário for igual ao meu, deixo aqui a minha solução, que não sei porque está a funcionar.

A solução que funcionou para mim foi da @Mina Samir, que está enviando a lista como uma lista mutável.

Meu cenário de problema:

-Carregando uma lista de amigos dentro de um fragmento.

  1. ActivityMain anexa o FragmentFriendList (Observa os dados vividos de itens de banco de dados de amigos) e, ao mesmo tempo, solicita uma solicitação http ao servidor para obter toda a minha lista de amigos.

  2. Atualize ou insira os itens do servidor http.

  3. Cada mudança acende o retorno de chamada onChanged do liveata. Mas, quando é a primeira vez que inicio o aplicativo, o que significa que não havia nada na minha mesa, a submitList é bem-sucedida sem nenhum tipo de erro, mas nada aparece na tela.

  4. No entanto, quando é a segunda vez que inicio o aplicativo, os dados estão sendo carregados na tela.

A solução é, conforme mencionado acima, submeter a lista como uma mutableList.

Março 3 abril 4
fonte
1

O motivo pelo qual ListAdapter .submitlist não é chamado é porque o objeto que você atualizou ainda mantém o mesmo endereço na memória.

Quando você atualiza um objeto com, digamos .setText, ele altera o valor do objeto original.

Assim, quando você verificar se object.id == object2.id, ele retornará como o mesmo porque ambos têm uma referência ao mesmo local na memória.

A solução é criar um novo objeto com os dados atualizados e inseri-lo em sua lista. Então submitList será chamado e funcionará corretamente

gamedev-il
fonte
0

Eu precisava modificar meus DiffUtils

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

Para realmente retornar se o conteúdo é novo, não basta comparar o id do modelo.

tonisivas
fonte
0

Usar a primeira resposta @RJFares atualiza a lista com sucesso, mas não mantém o estado de rolagem. O todo RecyclerViewcomeça na 0ª posição. Como alternativa, fiz o seguinte:

   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

   }

Dessa forma, posso manter o estado de rolagem RecyclerViewjunto com os dados modificados.

iCantC
fonte