I have a RecyclerView with an adapter that uses AssyncListDiffer. The problem I have is that the recyclerview is not updated when changes happen in the LiveData. The observer is notified but the list doesn't update.
This is my adapter:
class HourAdapter(private val interaction: HourInteraction? = null) :
RecyclerView.Adapter<HourAdapter.HourViewHolder>() {
private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
fun submitList(list: List<Hour>?) {
differ.submitList(list)
}
private fun getHourAt(position: Int): Hour {
return differ.currentList[position]
}
override fun getItemCount(): Int {
return differ.currentList.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HourViewHolder {...}
override fun onBindViewHolder(holder: HourViewHolder, position: Int) {...}
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Hour>() {
override fun areItemsTheSame(oldItem: Hour, newItem: Hour): Boolean {
return (oldItem.name == newItem.name) && (oldItem.isChecked == newItem.isChecked)
}
override fun areContentsTheSame(oldItem: Hour, newItem: Hour): Boolean {
return oldItem == newItem
}
}
class HourViewHolder
(
internal val binder: HourItemBinding
) : RecyclerView.ViewHolder(binder.root)
}
I use the "submitList()" to submit the new list. But it doesn't work.
I looked for solutions online and basically there were 2 options:
In the submitList function, call the "submitList" of AssyncListDiffer twice like this:
differ.submitList(null)
differ.submitList(list)
}```
The second option was to use ListAdapter and override the "submitList" function like this:
override fun submitList(list: List<Hour>?) {
super.submitList(list?.let { ArrayList(it) })
}
The first solution works, but the recyclerview blinks whenever I update it.
The second solution to override the method does not work for me.
I've been trying to fix this for days, but I can't make it work. Also, I don't want to use notifyItemChanged() or notifyDataSetChanged().
Is there another way?
I came around the same thing and observed the following.
Each time AsyncListDiffer received my list; it was the same object as previously - present in memory. Hence, the differ decided nothing changed and did not submit the updated list.
My list contained one object inside, and for each submission attempt I was changing one field. The object and the list of course remained the same.
So, I wondered why option number 2 did not work, and turned out that I needed to be a little more expressive:
submitList(it.map {
it.copy()
})
Otherwise, Kotlin would not make a deep copy of the object.
I was trying to delete a row and had the same problem, but solved it with the following:
The below code is added in the fragment:
private void deleteRow(int position) {
ArrayList<Card> cardsArrayList = adapter.getArrayList();
cardsArrayList.remove(position);
adapter.submitList(cardsArrayList);
}
And this code is in the adapter:
public ArrayList<Card> getArrayList(){
List<Card> cardList = mDiffer.getCurrentList();
return new ArrayList<>(cardList) ;
}
Related
I have a RecyclerView showing movie categories fetched with retrofit from an api.
Based on it's preferences the user should be able to show (checkbox is checked) or hide (checkbox is unchecked) categories.
Those setting should proceed in Fragment A. After clicking on a save button (or something similar) the check-state of the items/categries should be saved (for next app starts) and the checked items should be passed in a new list that it's showed in a "new" Recyclerview in Fragment B.
I read about Recyclerview-Selection, but it's hard to find a clear step by step tutorial, that's following a similar process as I need (and most of them are written in Java, as I am a completely programming beginner it's even harder to understand it).
So I hope that someone here can help me to get on the right way, be able to implement this also for tv-rv and series-rv.
This is my current adapter, nothing special (in my item-layout there is also a checkbox, id = rvCheckBox) but I am not sure on how to implent it here.
class MovieCategoryAdapter : ListAdapter<Data, MovieCategoryAdapter.ViewHolder>(
MOVIE_CATEGORY_COMPERATOR) {
inner class ViewHolder(val binding: RvItemMoviecategoryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(category: Data) {
binding.apply {
rvItemMoviecategory.text = category.title
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
RvItemMoviecategoryBinding.inflate(
LayoutInflater.from(
parent.context
),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val moviegenre = getItem(position)!!
holder.bind(moviegenre)
holder.binding
}
companion object {
private val MOVIE_CATEGORY_COMPERATOR = object : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Data, newItem: Data) =
oldItem == newItem
}
}
}
Faced such a problem: I have a RecyclerView, the data for which I get from the ViewModel using StateFlow:
viewModel.items.collect {
setRecyclerView(items)
}
Then, let's say, somewhere inside the Fragment, I change the data for items and there are more of them. In order for my RecyclerView to see my changes, I have to call the setRecyclerView(items) function again, which, it seems to me, can lead to the most unexpected consequences (for example, items will be duplicated). The question is: is it possible to somehow change the data and update the RecyclerView (including the onBindViewHolder function in it) without yet another creation of an Adapter?
Let's start talking about the adapter implementation. Reading your question, I believe you used RecyclerView.Adapter to implement your adapter. But there is another option that is simpler and more performant than this. It's the ListAdapter:
The most interesting thing about ListAdapter is the DiffUtil, that have a performative way to check if any item on your list was updated, deleted, or included. Here's a sample of the implementation:
abstract class MyAdapter: ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = ItemSimplePosterBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
class MyViewHolder(
private val binding: ItemSimplePosterBinding
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// Here you can get the item values to put these values on your view
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compares like this below
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// compare the objects
return oldItem == newItem
}
}
}
}
So, when your list is updated, you just have to call the submitList from the adapter, like this:
viewModel.items.collectLatest { items ->
// You will send the items to your adapter here
adapter.submitList(items)
}
Then, your RecyclerView just has to be configured on onViewCreated for example, and your list can be defined and updated in another place, observing the items change from ViewModel.
I am in the process of improving my app stability and performance, but right now I am stuck at a warning from Android Studio. Please consider the following Adapter class:
private class CoinsAdapter(private val fragment: CoinFragment, private val coins: List<Coin>): RecyclerView.Adapter<CoinsAdapter.ViewHolder>(), Filterable {
private val filter = ArrayList(coins)
override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val coin = filter[position]
holder.binding.coinImage.setImageResource(coin.image)
holder.binding.coinText.text = builder.toString()
}
override fun getItemCount() = filter.size
override fun getFilter() = object : Filter() {
override fun performFiltering(constraint: CharSequence): FilterResults {
if (constraint.length < 2) return fetchResults(coins)
val pattern = constraint.toString().lowercase().trim()
val filter = arrayListOf<Coin>()
for (coin in coins) if (coin.name.lowercase().contains(pattern)) filter.add(coin)
return fetchResults(filter)
}
private fun fetchResults(coins: List<Coin>): FilterResults {
val results = FilterResults()
results.values = coins
return results
}
override fun publishResults(constraint: CharSequence, results: FilterResults) {
filter.clear()
filter.addAll(results.values as List<Coin>)
notifyDataSetChanged()
}
}
private inner class ViewHolder(val binding: ItemCoinBinding) : RecyclerView.ViewHolder(binding.root)
}
The adapter and filter work perfectly but notice the publishResults function. Android Studio is warning that regarding the notifyDataSetChanged.
It will always be more efficient to use more specific change events if you can. Rely on notifyDataSetChanged as a last resort.
However, I am clueless on how to use the notifyDataSetChanged in this instance (with a filter). What would be the right method and how to use it in this case?
To the best of my knowledge, there's no point in using the Filterable interface with RecyclerView.Adapter. Filterable is intended for use in AdapterView Adapters because there are a few widgets that check if the Adapter is a Filterable and can automatically provide some filtering capability. However, RecyclerView.Adapter has no relation whatsoever to AdapterView's Adapter.
You can still use the Filter interface as a way to organize your code if you like, but to me it seems like needless extra boilerplate. I have seen other old answers on StackOverflow saying to implement Filterable in RecyclerView.Adapter, but I think they are doing it out of habit from working with the old Adapter class.
As for improving the performance of your adapter when filtering, there are a couple of options.
Use SortedList and SortedList.Callback to manage your list. The callback has you implement a bunch of functions to notify changes of specific items or ranges of items instead of the whole list at once. I have not used this, and it seems like there's a lot of room for getting something wrong because there are so many callback functions to implement. It's also a ton of boilerplate. The top answer here describes how to do it, but it's a few years old so I don't know if there's a more up-to-date way.
Extend your adapter from ListAdapter. ListAdapter's constructor takes a DiffUtil.ItemCallback argument. The callback tells it how to compare two items. As long as your model items have unique ID properties, this is very easy to implement. When using ListAdapter, you don't create your own List property in the class, but instead let the superclass handle that. Then instead of setting a new filtered list and calling notifyDataSetChanged(), you call adapter.submitList() with your filtered list, and it uses the DiffUtil to automatically change only the views necessary, and it does it with nice animations, too. Note you don't need to override getItemCount() either since the superclass owns the list.
Since you are filtering items, you might want to keep an extra property to store the original unfiltered list and use that when new filters are applied. So I did create an extra list property in this example. You need to be careful about only using it to pass to submitList() and always using currentList in onBindViewHolder() since currentList is what's actually being used by the Adapter to display.
And I removed the Filterable function and made it so the outside class can simply set the filter property.
class CoinsAdapter : ListAdapter<Coin, CoinsAdapter.ViewHolder>(CoinItemCallback) {
object CoinItemCallback : DiffUtil.ItemCallback<Coin>() {
override fun areItemsTheSame(oldItem: Coin, newItem: Coin): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Coin, newItem: Coin): Boolean = oldItem == newItem
}
var coins: List<Coin> = emptyList()
set(value) {
field = value
onListOrFilterChange()
}
var filter: CharSequence = ""
set(value) {
field = value
onListOrFilterChange()
}
override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val coin = currentList[position]
holder.binding.coinImage.setImageResource(coin.image)
holder.binding.coinText.text = builder.toString()
}
private fun onListOrFilterChange() {
if (filter.length < 2) {
submitList(coins)
return
}
val pattern = filter.toString().lowercase().trim()
val filteredList = coins.filter { pattern in it.name.lowercase() }
submitList(filteredList)
}
inner class ViewHolder(val binding: ItemCoinBinding) : RecyclerView.ViewHolder(binding.root)
}
The notifyDataSetChanged redraws the whole view and that's why Android Studio shows a warning.
To get away with this you could use DiffUtil
private class CoinsAdapter(private val fragment: CoinFragment, private val coins: List<Coin>): RecyclerView.Adapter<CoinsAdapter.ViewHolder>(FilterDiffCallBack()), Filterable {
....
....
//This check runs on background thread
class FilterDiffCallBack: DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Coin, newItem: Coin): Boolean {
return oldItem.someUniqueId == newItem.someUniqueId
}
override fun areContentsTheSame(oldItem: Coin, newItem: Coin): Boolean {
return oldItem == newItem
}
}
...
...
override fun publishResults(constraint: CharSequence, results: FilterResults) {
submitList(results)// call the DiffUtil internally
}
}
If the data in list mostly changes with user interaction then you could use methods like notifyItemChanged(int), notifyItemInserted(int), notifyItemRemoved(int), etc as this is the most efficient way to update your view. More info here
I am new in Paging 3 library in android kotlin. I want unlimited data. So I found Paging 3 library is helpful in my case. I used PagingSource to create a list. I am not using Room. I have nested recyclerView. I am using PagingDataAdapter with diff util for my Recyclerview. I used the recommended tutorial for Paging library from codelab and I succeeded without any problem. I am facing difficult to update the item. I used paging source to create list and inside list i have some data which are coming from sever. I completely all this without any problem. But how to update adapter or notify data has changed in reyclerview. I already mechanism to fetch updated list. I searched how to update the adapter in some place but every where is mention to use invalidate() from DataSource. DataSource is used in paging 2 right?. Now this is inside the Paging 3 as per Documentation in Migrate to Paging 3. I used Flow to retrieve data. This code is inside viewmodel class.
fun createRepo(data: List<String>, repository: RepositoryData): Flow<PagingData<UnlimitData>> {
return repository.getStreamData(data).cachedIn(viewModelScope)
}
I am passing list, which is coming from sever. getStreamData function return the items with int and string data. My Data class
data class UnlimitData(val id: Int, val name: String)
createRepo is calling in my activity class to send data in adpater.
lifecycleScope.launch {
viewModel.createRepo(serverData,repository).collectLatest { data ->
adapter.submitData(data)
}
}
This is my Adapter code:-
class unlimitedAdapter() :
PagingDataAdapter<UnlimitData, RecyclerView.ViewHolder>(COMPARATOR) {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
(holder as UnlimitedViewHolder).bind(item)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return UnlimitedViewHolder.create(parent)
}
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<UnlimitData>() {
override fun areItemsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean = oldItem == newItem
}
}
}
I added logic to insert/Update data in list using RetroFit. My list is updated successfully, but i am unable to refresh reyclerview.
Thanks in advance.
In order for paging to pick up new items, you will need to call PagingSource.invalidate() to inform Pager that it needs to fetch a new PagingSource and reload pages. You'll want to keep track of all the PagingSources your factory produces and invalidate them anytime you update the backing dataset from network.
EDIT: Something like this, but this is a very rough prototype
abstract class InvalidatingPagingSourceFactory<K,V> : () -> PagingSource<K,V> {
private val list = mutableListOf()
abstract fun create()
override final fun invoke() {
create().also { list.add(it) }
}
fun invalidate() {
while (list.isNotEmpty()) { list.removeFirst().invalidate() }
}
}
When We use adapter.refresh() method it will refresh the latest data and bind with the layout.
But It is not reLoading the recycler view with Position.
In that case, you will get the wrong position.
Because in DiffUtils you match correctly. So, whenever you refresh, it will bind only that new and not bonded data.
So, The solution is you have to make false the DiffUtils. In that case, PageSource will update the full list and the position will be updated.
Like : oldItem.id ==-1
private val COMPARATOR = object : DiffUtil.ItemCallback<UnlimitData>() {
override fun areItemsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean =
oldItem.id ==-1
override fun areContentsTheSame(oldItem: UnlimitData, newItem: UnlimitData): Boolean = oldItem == newItem
}
I'm able to retrieve data from Firestore (it is definetly available inside the GlobalScope of populateValletList() and I was able to populate my RecyclerView from an asnyc database call or when I simply added a Vallet to my items list manually inside populateValletList(), however, when I want to populate that View from Firestore data it doesn't work. The
onBindViewHolder in RecyclerAdapter doesn't get called anymore
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder){
is ValletViewHolder ->{
holder.bind(items[position])
}
}
}
that's how I populate my items private var items : MutableList<Vallet> = ArrayList() :
fun populateValletList() {
GlobalScope.launch {
items = getAllValletsFromDatabase.executeUseCase()
}
}
I init my RecyclerView inside onActivityCreated
private fun initRecyclerView(){
recycler_view_vallets.apply{
layoutManager = LinearLayoutManager(context)
addItemDecoration(ValletRecyclerAdapter.ValletItemDecorator(30))
valletAdapter = ValletRecyclerAdapter()
adapter = walletAdapter
}
valletAdapter.populateValletList()
}
This is my first time working with coroutines, what am I overlooking here?
Ok, I think there is some improvements that you could in your code, but the reason I believe is not working the way you want is because I you update your Items List, you have to call notifyDataSetChanged() on your adapter.
Ideally u should run ur coroutine using a scope that is not global, to avoid leaks, you can use a viewmodel for it, or you could use lifecycleScope.run { } in ur fragment, for that I believe you will need to add a depedency.
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
And regarding update your recycler, I recommend using ListAdapter, so it adds DIFF Utils and makes easier to update values.
To sum up.
Ur Recycler Adapter would be like this:
class HomePatchesAdapter : ListAdapter<Vallet, RecyclerView.ViewHolder>(REPO_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return HomePatchesViewHolder.create(parent, viewType)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val repoItem = getItem(position)
if (repoItem != null) {
(holder as HomePatchesViewHolder).bind(repoItem)
}
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Vallet>() {
override fun areItemsTheSame(oldItem: Vallet, newItem: Vallet): Boolean =
oldItem.name == newItem.name
override fun areContentsTheSame(oldItem: Vallet, newItem: Vallet): Boolean =
oldItem == newItem
}
}
}
and ur fragment would be like this:
private fun initRecyclerView(){
recycler_view_vallets.apply{
layoutManager = LinearLayoutManager(context)
addItemDecoration(ValletRecyclerAdapter.ValletItemDecorator(30))
}
val adapter = HomePatchesAdapter()
recycler_view_vallets?.adapter = adapter
lifecycleScope.run {
adapter.submitList(getAllValletsFromDatabase.executeUseCase())
}
}
Let me know if it makes sense.