diffutil callback in recyclerView doesn't work perfectly - android

I'm trying to add some search on the RecyclerView list without using
notifyDataSetChanged()
instead to it using
diffutil.callback()
but the issue is that it change the list correctly but it doesn't change the UI correctly
Here is my code and I will explain it
class RecordsAdapter : RecyclerView.Adapter<RecordsAdapter.ViewHolder>() {
var adapterList = listOf<CustomerModel>()
var modelList = listOf<CustomerModel>()
set(value) {
adapterList = value
field = value
}
private var modelListFiltered = listOf<CustomerModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(CustomerCellBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(adapterList[position])
}
override fun getItemCount(): Int = adapterList.size
fun filter(isFiltered: Boolean, filterSearch: String) {
if (isFiltered) {
val filter = modelList
.filter {
it.name.contains(filterSearch) || it.id.contains(filterSearch)
}
modelListFiltered = filter
}
adapterList = if (isFiltered) modelListFiltered else modelList
val diff = CartDiffUtil(
if (isFiltered) modelList else modelListFiltered,
if (isFiltered) modelListFiltered else modelList
)
DiffUtil.calculateDiff(diff).dispatchUpdatesTo(this)
}
inner class ViewHolder(private var binding: CustomerCellBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(model: CustomerModel) {
binding.let {
it.model = model
it.executePendingBindings()
}
}
}
}
class CartDiffUtil(private val oldList: List<CustomerModel>, private val newList: List<CustomerModel>) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].id == newList[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]
}
So I'm calling filter function to filter and I'm sending two parameters on if there is any filter and the second is the search.
Now the issue appears in this scenario
0. searching ""
1. searching "testing 2"
2. searching "testing 4"
3. searching "testing 2"
4. searching ""
As you can see in the images, when I search for "testing 2" after "testing 4" it keeps showing "testing 4" and even if I clear the search it gives me two cells of "testing 4" instead of one "testing 2" and one "testing 4"
Hope my question is clear.
Thanks.

I'm guessing your juggling of three list properties is leading to some situations where there can be the same list instance in the before and after of the DiffUtil so it cannot successfully compare them.
Also, it's much easier to use ListAdapter instead of RecyclerView.Adapter when you want to use DiffUtil. Note that when you use ListAdapter, you use ItemCallback instead of Callback. ItemCallback is simpler.
Try doing it this way, where there is only the modelList and when it or the filter changes, you determine what the new list is and submit it to the ListAdapter and let it handle the changes.
class RecordsAdapter : ListAdapter<CustomerModel, RecordsAdapter.ViewHolder>(CustomerModelCallback) {
var modelList = listOf<CustomerModel>()
set(value) {
field = value
resync()
}
private var filterText: String = ""
private var isFiltered: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(CustomerCellBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(adapterList[position])
}
fun filter(isFiltered: Boolean, filterText: String = "") {
this.isFiltered = isFiltered
this.filterText = filterText
resync()
}
private fun resync() {
val newList = when {
isFiltered && filterText.isNotEmpty() ->
modelList.filter {
it.name.contains(filterSearch) || it.id.contains(filterSearch)
}
else -> modelList
}
submitList(newList)
}
// view holder...
}
object CustomerModelCallback : DiffUtil.ItemCallback<CustomerModel>() {
override fun areItemsTheSame(oldItem: CustomerModel, newItem: CustomerModel): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: CustomerModel, newItem: CustomerModel): Boolean =
oldItem == newItem
}

Related

Android Paging 3 library - for paging data from Room database, flow becomes empty on data change

I am trying to use android Paging 3 (version 3.0.0-SNAPSHOT) for paging data from Room database(no remote data source).
Initially the page loads data successfully , but when a new "Entry" is added to database and I return to this page,collectLatest is fired but no data is loaded ("pagingData" entry list is empty )
this is my query :
#Query("SELECT * FROM entry ORDER BY dateTime DESC")
fun getAll() : PagingSource<Int, Entry>
and this is my viewmodel:
val flow = Pager(
PagingConfig(pageSize = 20)
) {
entryDao.getAll()
}.flow
.cachedIn(viewModelScope)
in my fragment , I am observing the data like this :
iewLifecycleOwner.lifecycleScope.launch {
homeViewModel.flow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
and this is my adapter:
class EntriesAdapter( val context : Context, private val onClick: (String)->Unit) :
PagingDataAdapter<Entry , RecyclerView.ViewHolder>(diffCallback)
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryViewHolder
= EntryViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_entry, parent, false
) , onClick)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
var entry = getItem(position)
if (entry != null) {
(holder as EntryViewHolder).bindTo(entry);
}
}
companion object {
//This diff callback informs the PagedListAdapter how to compute list differences when new
private val diffCallback = object : DiffUtil.ItemCallback<Entry>() {
override fun areItemsTheSame(oldItem: Entry, newItem: Entry): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Entry, newItem: Entry): Boolean =
oldItem.id == newItem.id
}
}
inner class EntryViewHolder( val binding : ItemEntryBinding ,val onCLick:
(String)->Unit ) : RecyclerView.ViewHolder(binding.root) {
var entry:Entry? = null
fun bindTo(entry: Entry) {
this.entry = entry
with(binding) {
entryItem = entry
cardView.setOnClickListener{
onCLick
}
executePendingBindings()
}
}
}
}
I had a similar problem to yours and I just forgot to call entriesAdapter.refresh() after add new item.

Android: SVG Drawables Stop Working or Glitch in RecyclerView Lists

I am having an issue where I imported some SVG drawables (that are optimised in Illustrator and have short path data - so their complexity is out of discussion) and displayed them in RecyclerView items. The problem is that, after testing the application many times, they stop working or they start rendering with glitches (like missing chunks or shapes). Weirdly enough, an app cache wipe resolves the issue and they work normally until after I ran the app from Android Studio about 5-6 times.
Here is what I mean by 'stopped working' :
In one activity they appear as red warnings, in another one they appear as a fingerprint icon (tho I do not have such an icon in the entire project, nor fingerprint implementation).
Here is the implementation:
I add the entries in room database like this:
Category(icon = R.drawable.ic_category_homepage)
where a category data class looks like this:
#Entity(tableName = "categories")
data class Category(
val title: String,
#DrawableRes
val icon: Int
)
So I add the SVG drawable reference as a DrawableRes Int in the local storage. Then, when I'm displaying the icon in the adapter, I use Glide:
Glide.with(context)
.load(category.icon)
.transition(DrawableTransitionOptions.withCrossFade())
.into(itemView.categoryIV)
Here is the entire adapter:
class DrawerAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val categories: ArrayList<Category> = ArrayList()
fun submitCategories(newFeed: List<Category>, lifecycleCoroutineScope: LifecycleCoroutineScope) {
lifecycleCoroutineScope.launch {
val result = coroutineRunOnComputationThread {
val oldFeed = categories
val result: DiffUtil.DiffResult = DiffUtil.calculateDiff(
DrawerDiffCallback(oldFeed, newFeed)
)
categories.clear()
categories.addAll(newFeed)
result
}
coroutineRunOnMainThread {
result.dispatchUpdatesTo(this#DrawerAdapter)
}
}
}
override fun getItemCount(): Int = categories.size
override fun getItemId(position: Int): Long {
return if (categories.isNullOrEmpty()) 0 else categories[position].id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DrawerItemViewHolder(parent.inflate(R.layout.item_drawer_menu))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =
(holder as DrawerItemViewHolder).bind(categories[position])
inner class DrawerItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(category: Category) = with(itemView) {
Glide.with(context)
.load(category.icon)
.transition(DrawableTransitionOptions.withCrossFade())
.into(itemDrawerIVIcon)
if (category.preConfigured && category.resTitle != null)
itemDrawerTVTitle.text = context.resources.getString(category.resTitle)
else
itemDrawerTVTitle.text = category.title
}
}
private inner class DrawerDiffCallback(
private var oldFeed: List<Category>,
private var newFeed: List<Category>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldFeed[oldItemPosition]
val newItem = newFeed[newItemPosition]
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldFeed[oldItemPosition]
val newItem = newFeed[newItemPosition]
return oldItem == newItem
}
override fun getOldListSize(): Int = oldFeed.size
override fun getNewListSize(): Int = newFeed.size
}
}
Any idea why I get this weird behavior?
Hope this will resolve your glitching issue.
Picasso.get().load(category.icon)
.error(R.drawable.placeholder_round)
.placeholder(R.drawable.placeholder_round)
.resize(100, 100)
.into(itemDrawerIVIcon)
Just replace your Glide with Picasso with above config

Recyclerview DiffUtil Item Update

I have endless scroll in my recyclerview, so, it will update when there is new data. and i am using DiffUtil to update data in the recyclerview. DiffUtil does updates the data but whenever there is update data, recyclerview scroll to top and what it looks like is "using the notifydatasetchanged()". here is my DiffUtil and my adapter to update data.
class ProductDiffUtil(
val oldProductList: List<ProductModel>, val newProductList: List<ProductModel>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldProductList[oldItemPosition].id == newProductList[newItemPosition].id
}
override fun getOldListSize(): Int = oldProductList.size
override fun getNewListSize(): Int = newProductList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldProductList[oldItemPosition] == newProductList[newItemPosition]
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
return super.getChangePayload(oldItemPosition, newItemPosition)
}
}
Here is my adapter to update data
fun addProductList(productList: List<ProductModel>?) {
val diffResult = DiffUtil.calculateDiff(ProductDiffUtil(this.productList, productList!!))
this.productList.addAll(productList)
diffResult.dispatchUpdatesTo(this)
}
please help me with this. it is working fine when i am using notifyItemRangeChanged()... so what should i use to update data in recyclerview for best practice.
https://drive.google.com/open?id=1SapXW2wusmTpyGCRA9fa0aSLCYNL1fzN
You're comparing the previous contents against only the new items, rather than against the list with all of them added.
Imagine if this.productList is currently 1,2,3, and the new productList is 4,5,6. When you run
DiffUtil.calculateDiff(ProductDiffUtil(this.productList, productList!!)
It will compare 1 to 4, 2 to 5, etc. and conclude that everything has changed and no new items have been added. (note: this is an oversimplification of the DiffUtil algorithm, but serves to illustrate the point)
Instead, if you want to use DiffUtil:
val oldList = ArrayList(productList)
this.productList.addAll(productList)
val diffResult = DiffUtil.calculateDiff(ProductDiffUtil(oldList, productList!!)
diffResult.dispatchUpdatesTo(this)
or, since you know exactly how many items are added and where, just use notifyItemRangeInserted and avoid the copy:
val oldSize = this.productList.size
this.productList.addAll(productList)
notifyItemRangeInserted(oldSize, productList.size)
Consider making a generic diffUtil class instead of creating it for each adapter.
fun <T>diffList(oldList: List<T>, newList: List<T>, sameItem: (a: T, b: T) -> Boolean): DiffUtil.DiffResult {
val callback: DiffUtil.Callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return sameItem(oldList[oldItemPosition], newList[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
return DiffUtil.calculateDiff(callback) }
You can use it in your adapter like this:
fun setItems(products: List<Product>) {
val oldList = productList
productList = products
diffList(oldList, products, sameItem = { a, b -> a.id == b.id }).dispatchUpdatesTo(this)
}
Check if the layout manager has already been set and get the current scroll position. Like this:
var itemPostion= 0
if(myRecyclerView.layoutmanager != null){
itemPostion = (myRecyclerView.layoutmanager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition()
}
You can have a look at this sample project on GitHub

Adapter data mutableList crash when modified

I'm making a SearchView Filter for a RecyclerView, in my Filter object, in the fun 'publishResults', my app crash because i'm trying to modify a mutableListOf, but the data.clear() and data.addAll() cause an error
Every modification to the list seems to make the app crash, I've never seen this error despite all other list I've tried to modify. I think the problem come from the fact that it's used by the recyclerView but I have no clue why, I've find nothing on internet and even less with kotlin language.
Also it seems like there is a big problem when trying to cast the results?.values to a Collection.
here is my full Adapter code
class SmallCatchAdapter(val clickListener: FishModelListener) : RecyclerView.Adapter<SmallCatchAdapter.ViewHolder>(), Filterable {
var data = mutableListOf<FishModel>()
set(value) {
field = value
notifyDataSetChanged()
}
var dataFull = listOf<FishModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun getItemCount() = data.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.bind(item, clickListener)
}
class ViewHolder private constructor(val binding: ItemListSmallCatchBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item : FishModel, clickListener: FishModelListener) {
binding.fishModel = item
binding.clickListener = clickListener
binding.executePendingBindings()
}
companion object{
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemListSmallCatchBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
override fun getFilter(): Filter {
return fishListFilter
}
private val fishListFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val dataFiltered = mutableListOf<FishModel>()
if (constraint == null || constraint.isEmpty()) {
dataFiltered.addAll(dataFull)
} else {
val filterPattern: String = constraint.toString().toLowerCase().trim()
for (item in dataFull) {
if(item.name.toLowerCase().contains(filterPattern)) {
dataFiltered.add(item)
}
}
}
val results = FilterResults()
results.values = dataFiltered
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
data.clear()
data.addAll(results?.values as MutableList<FishModel>)
notifyDataSetChanged()
}
}
}
class FishModelListener(val clickListener: (fishModelId: Int) -> Unit) {
fun onClick(fishModel: FishModel) = clickListener(fishModel.speciesID)
}
Where it crashes
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
data.clear()
data.addAll(results?.values as Collection<FishModel>)
notifyDataSetChanged()
}
The error:
java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)
at java.util.AbstractList$Itr.remove(AbstractList.java:374)
at java.util.AbstractList.removeRange(AbstractList.java:571)
at java.util.AbstractList.clear(AbstractList.java:234)
Everything worked well before i implement this functionality, now I've also noticed that this bug has somehow messed with my Room Query, one (and only one) of my coroutineScope is never launched, everythings else is ok
(Sorry for the mistakes, i'm not english)
Instead of using data inside onBindViewHolder you should create another list variable and use it
var filterList : mutableListOf<FishModel>()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = filterList[position]
holder.bind(item, clickListener)
}
For the filter I am showing some references, follow a similar way as below:
private val customFilter: Filter = object : Filter() {
override fun performFiltering(constraint: CharSequence): FilterResults {
filterList =
if (constraint.isEmpty()) {
items.toMutableList()
} else {
items.filter {
it.name.contains(constraint, true) ||
it.name == constraint ||
it.price.toString() == constraint
}
.toMutableList()
}
if (sortByPV) {
filterList.sortByDescending { it.pointValue }
}
return FilterResults().apply {
values = filterList
count = filterList.size
}
}
override fun publishResults(constraint: CharSequence, results: FilterResults) {
notifyDataSetChanged()
}
}
override fun getFilter(): Filter = customFilter
override fun getItemCount() = filterList.size
Here, you may need to update your code accordingly for item click listener.

RealmBaseAdapter with section headers

I'm trying to use the realm data store for Android and i'm trying to build an application that shows the user a list of options in a ListView, kind of like the users contact list. The first letter for each section (such as A, B, C, etc.) should be a header. Is there a way to achieve this with RealmBaseAdapter?
Currently I have it working with ArrayAdapter and I simply have an array with values populated but would like to pull data from Realm using its adapter if possible. I know in iOS this is fairly straight forward using the NSFetchedResultsController. How do we break up the RealmResults into sections?
RealmBaseAdapter doesn't work with ExpandableListAdapter (which I am assuming you are using for sections?), so right now your only choice is creating your own implementation. But a RealmResults is also a List so it should work seamlessly with an ArrayAdapter.
You can also see more details here: https://github.com/realm/realm-java/issues/978
This is what I use:
package com.poterion.android.library.adapters
import android.widget.BaseExpandableListAdapter
import io.realm.*
/**
* #author Jan Kubovy <jan#kubovy.eu>
*/
abstract class RealmExpandableListAdapter<out Group : Any, Item : RealmModel>(
private val itemGroupsProvider: (Item) -> Collection<Group?>,
private val groupsProvider: (Collection<Item>) -> List<Group?>,
private var adapterData: OrderedRealmCollection<Item>?) : BaseExpandableListAdapter() {
private val listener: RealmChangeListener<OrderedRealmCollection<Item>>?
protected val groups: List<Group?>
get() {
return adapterData?.takeIf { isDataValid }?.let(groupsProvider) ?: emptyList()
}
private val isDataValid: Boolean
get() = adapterData?.isValid == true
init {
if (adapterData?.isManaged == false)
throw IllegalStateException("Only use this adapter with managed list, for un-managed lists you can just use the BaseAdapter")
this.listener = RealmChangeListener { notifyDataSetChanged() }
adapterData?.takeIf { isDataValid }?.also { addListener(it) }
}
private fun addListener(data: OrderedRealmCollection<Item>) {
when (data) {
is RealmResults<Item> -> data.addChangeListener((listener as RealmChangeListener<RealmResults<Item>>))
is RealmList<Item> -> data.addChangeListener((listener as RealmChangeListener<RealmList<Item>>))
else -> throw IllegalArgumentException("RealmCollection not supported: " + data.javaClass)
}
}
private fun removeListener(data: OrderedRealmCollection<Item>) {
when (data) {
is RealmResults<Item> -> data.removeChangeListener((listener as RealmChangeListener<RealmResults<Item>>))
is RealmList<Item> -> data.removeChangeListener((listener as RealmChangeListener<RealmList<Item>>))
else -> throw IllegalArgumentException("RealmCollection not supported: " + data.javaClass)
}
}
override fun getGroupCount(): Int = groups.size
override fun getChildrenCount(groupPosition: Int): Int = adapterData?.takeIf { isDataValid }?.let { data ->
val g = groups[groupPosition]
data.filter { g == null || groups(it).contains(g) }.size
} ?: 0
override fun getGroup(groupPosition: Int): Group? = if (groups.size > groupPosition) groups[groupPosition] else null
override fun getChild(groupPosition: Int, childPosition: Int): Item? = children(groupPosition)
.takeIf { it.size > childPosition }?.get(childPosition)
override fun notifyDataSetChanged() {
super.notifyDataSetChanged()
}
private fun children(groupPosition: Int): List<Item> {
return getGroup(groupPosition)
?.let { g -> adapterData?.takeIf { isDataValid }?.filter { groups(it).contains(g) } } ?: emptyList()
}
}
And usage:
class PersonListAdapter(realm: Realm) :
RealmExpandableListAdapter<String, Person>(
itemGroupsProvider = { person -> arrayOf(person.group, null) },
groupsProvider = { people -> people.map { it.group } },
adapterData = realm.where(Person::class.java)
.findAllSortedAsync("lastName", Sort.ASCENDING, "firstName", Sort.ASCENDING)) {
override fun getGroupId(groupPosition: Int) = getGroup(groupPosition).id
override fun getChildId(groupPosition: Int, childPosition: Int) = getChild(groupPosition, childPosition).id
override fun hasStableIds() = true
override fun getGroupView(groupPosition: Int, isExpanded: Boolean, convertView: View?, parent: ViewGroup?): View {
// ... Item View here ...
}
override fun getChildView(groupPosition: Int, childPosition: Int, isLastChild: Boolean,
convertView: View?, parent: ViewGroup?): View {
// ... Group View here ...
}
override fun isChildSelectable(groupPosition: Int, childPosition: Int) = true
}

Categories

Resources