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
Related
What I would want to do:
I would want to filter through list and show values that match the search phrase. Additionally I would want to show correct current list size in the UI.
What is the issue:
The issue is that I can filter through list, but on UI, my list size doesn't update. For example if I've downloaded 5 items to offline mode, it would show that there are still 5 items total, but there would be only 2 for example (and only 2 visible on the screen).
The next issue is that if I try to empty the search bar, the list doesn't go back to it's initial state. It's just empty and list size on UI shows that there are 5 items.
What I've tried:
I've tried adding notifyDataSetChanged() in adapter, but it doesn't work as intended. While debugging, The list is filtered and list after filtering is smaller, but it doesn't emit that value to the fragment.
Adapter:
class OssOfflineDevicesListAdapter(
private val offlineDevices: MutableList<OssOfflineDevicesI> = mutableListOf(),
private val removeDevicesFromQueue: (Long) -> Unit,
private val retryDownloadingDevices: (Long) -> Unit
) : RecyclerView.Adapter<OssOfflineDevicesListAdapter.OssOfflineDevicesListItemViewHolder>() {
private val filteredDevices: MutableList<OssOfflineDevicesI> = offlineDevices
override fun getItemCount(): Int = filteredDevices.size
fun filter(searchPhrase: String) {
val newOfflineDevices = offlineDevices.filter {
it.name().contains(searchPhrase, true)
}
updateListView(newOfflineDevices)
filteredDevices.clear()
filteredDevices.addAll(newOfflineDevices)
notifyDataSetChanged()
}
fun update(newValues: List<OssOfflineDevicesI>) {
updateListView(newValues)
filteredDevices.clear()
filteredDevices.addAll(newValues)
notifyDataSetChanged()
}
private fun updateListView(newValues: List<OssOfflineDevicesI>) {
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int = filteredDevices.size
override fun getNewListSize(): Int = newValues.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return filteredDevices[oldItemPosition].id() == newValues[newItemPosition].id()
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldDevices = filteredDevices[oldItemPosition]
val newDevices = newValues[newItemPosition]
return oldDevices.externalId() == newDevices.externalId() &&
oldDevices.downloadingStatus() == newDevices.downloadingStatus() &&
oldDevices.name() == newDevices.name()
}
}).dispatchUpdatesTo(this)
}
Fragment:
class OssOfflineDevicesListFragment : CoreFragment() {
private val disposableBag = CompositeDisposable()
private val viewModel by viewModel<OssOfflineDevicesListViewModel>()
private val offlineDevicesListAdapter = OssOfflineDevicesListAdapter(
removeDevicesFromQueue = { devicesExternalId -> removeDevicesFromQueue(devicesExternalId) },
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpUI()
observeOssActionTransmitter()
setUpQuickSearch()
viewModel.offlineDevices().observe(viewLifecycleOwner, { offlineDevices ->
if (offlineDevices.isNullOrEmpty()) {
showEmptyView()
} else {
showContentView(offlineDevices)
}
})
}
private fun setUpQuickSearch() {
search.searchEdit
.textChanges()
.skipInitialValue()
.skip(1, TimeUnit.SECONDS)
.debounce(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
offlineDevicesListAdapter.filter("$it")
}, {
offlineDevicesListAdapter.filter("")
})
.addTo(disposableBag)
}
private fun showEmptyView() {
recycler_view.gone()
empty_state_list_info.visible()
empty_list_state_image.visible()
updateResultCount(0)
}
private fun showContentView(offlineDevices: List<OssOfflineDevicesI>) {
empty_state_list_info.gone()
empty_list_state_image.gone()
offlineDevicesListAdapter.update(offlineDevices)
recycler_view.visible()
updateResultCount(offlineDevices.size)
}
private fun updateResultCount(resultCount: Int) {
search.countText.text = String.format("%s %d",
com.comarch.fsm.android.core.extensions.getString("total_results"), resultCount)
}
}
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
}
I'm performing some operations in my model list to be passed after to recycler adapter and update the adapter list. For some unexpected reason the adapter list has been updated before i pass the new list. I'm using DiffUtil to calculate the difference between an oldList and newList.
internal fun CheckoutDetailsActivity.updateLineItemWithPartialPayment(lineItemModelToUpdate: LineItemModel){
GlobalScope.launch {
withContext(Dispatchers.Main) {
calculateTodayPayments(lineItemModelToUpdate)
}
}
}
internal suspend fun CheckoutDetailsActivity.calculateTodayPayments(lineItemModelToUpdate: LineItemModel){
var todayPayments: BigDecimal = BigDecimal.ZERO
val lineItems: ArrayList<LineItemModel> = ArrayList(viewModel.ticket?.lineItems)
withContext(Dispatchers.Default){
lineItems.forEachIndexed loop#{ index, lineItemModel ->
if(lineItemModel.id == lineItemModelToUpdate.id){
lineItems[index] = lineItemModelToUpdate
todayPayments += lineItemModelToUpdate.partialPaymentAmount?: BigDecimal.ZERO
return#loop
}
lineItemModel.inPartialPayment = true
todayPayments += lineItemModel.partialPaymentAmount?: BigDecimal.ZERO
}
}
viewModel.todayPayments = todayPayments
updateRecyclerAdapter(lineItems)
showTodayPaymentContainer()
}
internal fun CheckoutDetailsActivity.updateRecyclerAdapter(lineItems: ArrayList<LineItemModel>){
viewModel.lineItemsWithPartialPayment.clear()
viewModel.lineItemsWithPartialPayment.addAll(lineItems)
viewModel.recyclerViewAdapter?.submitList(viewModel.sortLineItems(ArrayList(lineItems)))
viewModel.ticket?.lineItems = lineItems
}
this is my submitList() function in the adapter:
fun submitList(lineItemList: List<LineItemModel>){
val oldLineItemList = lineItems
val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(
LineItemDiffCallback(
oldLineItemList,
lineItemList
)
)
this.lineItems.clear()
this.lineItems.addAll(lineItemList)
diffResult.dispatchUpdatesTo(this)
}
class LineItemDiffCallback(
var oldLineItemList: List<LineItemModel>,
var newLineItemList: List<LineItemModel>
): DiffUtil.Callback(){
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldLineItemList[oldItemPosition].id == newLineItemList[newItemPosition].id
}
override fun getOldListSize(): Int {
return oldLineItemList.size
}
override fun getNewListSize(): Int {
return newLineItemList.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldLineItemList[oldItemPosition] == newLineItemList[newItemPosition]
}
}
In the last method i'm updating the adapter list with submitList(), when i check the adapter list in viewModel.todayPayments = todayPayments line it has been already updated with the previous operation "forEachIndexed()", for that reason DiffUtil doesn't works correctly, because it can't find difference between oldList and newList.
updateLineItemWithPartialPayment calls calculateTodayPayments that calls updateRecyclerAdapter that calls updateLineItemWithPartialPayment that calls calculateTodayPayments that calls updateRecyclerAdapter [...] you get the point...
Why are using extension functions? Aren't you calling them from the activity itself?
Another thing... is this submitList function yours or are you using a PagedListAdapter?
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
Wouldn't it be better to get the difference instead of returning the whole values for the UI to redraw?
var collection: List<String> by
Delegates.observable(emptyList()) { prop, old, new ->
notifyDataSetChanged()
}
is it possible to make it more efficient?
You should take a look to DiffUtil class
DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.
DiffUtil uses Eugene W. Myers's difference algorithm to calculate the minimal number of updates to convert one list into another. Myers's algorithm does not handle items that are moved so DiffUtil runs a second pass on the result to detect items that were moved.
If the lists are large, this operation may take significant time so you are advised to run this on a background thread,
Basically, you have to implement a DiffUtil.Callback using both lists,
data class MyPojo(val id: Long, val name: String)
class DiffCallback(
private val oldList: List<MyPojo>,
private val newList: List<MyPojo>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].name == newList[newItemPosition].name
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
// Implement method if you're going to use ItemAnimator
return super.getChangePayload(oldItemPosition, newItemPosition)
}
}
then you have to notify the adapter using it. for example, you could create a function in your adapter like this:
fun swap(items: List<myPojo>) {
val diffCallback = ActorDiffCallback(this.items, items)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.items.clear()
this.items.addAll(items)
diffResult.dispatchUpdatesTo(this)
}
In your case, supposing that collection is a member of your adapter:
var collection: List<String> by Delegates.observable(emptyList()) { prop, old, new ->
val diffCallback = DiffCallback(old, new)
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(this)
}
Some references:
https://developer.android.com/reference/android/support/v7/util/DiffUtil
https://proandroiddev.com/diffutil-is-a-must-797502bc1149
https://github.com/mrmike/DiffUtil-sample/