Android DiffUtil Doesn't Notify Properly - android

I was following along the Android Room With a View tutorial.
The tutorial makes use of DiffUtil to compute changes in the list and update the RecyclerView accordingly.
However, when removing or adding items to the RecyclerView, DiffUtil always causes the entire RecyclerView to reload, instead of calling the correct notifyItemRemoved or notifyItemInserted.
My Adapter:
class MarksAdapter(private val context: Context) :
ListAdapter<Mark, MarksAdapter.MarkViewHolder>(MarksComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MarkViewHolder {
return MarkViewHolder.create(parent)
}
override fun onBindViewHolder(holder: MarkViewHolder, position: Int) {
val mark = getItem(position)
holder.bind(context, mark)
}
class MarkViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private var mark: Mark? = null
fun bind(context: Context, mark: Mark) {
this.mark = mark
// Removed for brevity...
}
companion object {
fun create(parent: ViewGroup): MarkViewHolder {
val view: View =
LayoutInflater.from(parent.context).inflate(R.layout.card_view, parent, false)
return MarkViewHolder(view)
}
}
}
class MarksComparator : DiffUtil.ItemCallback<Mark>() {
override fun areItemsTheSame(oldItem: Mark, newItem: Mark): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Mark, newItem: Mark): Boolean {
return oldItem == newItem
}
}
}
From the activity/fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerViewMarks.apply {
marksAdapter = MarksAdapter(context)
adapter = marksAdapter
layoutManager = LinearLayoutManager(this#MarksOverviewFragment.requireContext())
}
marksViewModel.allMarks.observe(viewLifecycleOwner) { marks ->
marks?.let { marksAdapter.submitList(it) }
}
}

After thinking about it some more, the only place where I can actually influence what DiffUtil does, is within the DiffUtil.Callback.
So after changing:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newList[newItemPosition] === oldList[oldItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newList[newItemPosition] == oldList[oldItemPosition]
}
to:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newList[newItemPosition].Uid == oldList[oldItemPosition].Uid
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newList[newItemPosition] == oldList[oldItemPosition]
}
(notice the comparison of Uid instead of identity)
...the DiffUtil works as expected.
What I'm not sure however, is why it worked in the tutorial, but not for me.

Related

How to properly implement RecyclerView.ItemAnimator using the Lottie animation library?

I have a case where a view within RecyclerView items needs to be animated using the Lottie library. Each recycler view item is clickable and contains a liking Lottie animation.
I defined a custom RecyclerView.ItemAnimator like this:
class SampleItemAnimator : DefaultItemAnimator() {
override fun animateChange(
oldHolder: RecyclerView.ViewHolder,
newHolder: RecyclerView.ViewHolder,
preInfo: ItemHolderInfo,
postInfo: ItemHolderInfo
): Boolean {
val holder = newHolder as BindingViewHolder<ItemSampleBinding>
val animator = lottieAnimatorListener {
dispatchAnimationFinished(holder)
holder.binding.sampleAnimation.removeAllAnimatorListeners()
}
holder.binding.sampleAnimation.addAnimatorListener(animator)
if (preInfo is SampleItemHolderInfo) {
if (preInfo.isItemLicked) {
holder.binding.sampleAnimation.playAnimation()
} else {
resetAnimation(holder.binding.sampleAnimation)
}
return true
}
return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
}
private fun resetAnimation(lottieAnimationView: LottieAnimationView) {
lottieAnimationView.progress = 0f
lottieAnimationView.cancelAnimation()
}
override fun recordPreLayoutInformation(
state: RecyclerView.State,
viewHolder: RecyclerView.ViewHolder,
changeFlags: Int,
payloads: MutableList<Any>
): ItemHolderInfo {
if (changeFlags == FLAG_CHANGED) {
return produceItemHolderInfoOrElse(payloads.firstOrNull() as? Int) {
super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
}
}
return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
}
private fun produceItemHolderInfoOrElse(value: Int?, action: () -> ItemHolderInfo) =
when (value) {
LIKE_ITEM -> SampleItemHolderInfo(true)
UNLIKE_ITEM -> SampleItemHolderInfo(false)
else -> action()
}
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder) = true
override fun canReuseUpdatedViewHolder(
viewHolder: RecyclerView.ViewHolder,
payloads: MutableList<Any>
) = true
}
lottieAnimatorListener is just a function that creates Animator.AnimatorListener to tell RecyclerView when the animation is canceled or ended by calling dispatchAnimationFinished(holder).
Everything works except that sometimes the liking animation can randomly play on items with no likes, especially while scrolling RecyclerView too fast.
As far as I understand, it happens because the ItemAnimator re-uses the same view holders and either uses outdated ItemHolderInfo or does not notify the RecyclerView about the end of the animation correctly.
That is how I pass a payload to the adapter to tell what has changed using DiffUtil.Callback.
class SampleListDiffCallback : DiffCallback<SampleItem> {
override fun areContentsTheSame(oldItem: SampleItem, newItem: SampleItem) =
oldItem.markableItem == newItem.markableItem
override fun areItemsTheSame(oldItem: SampleItem, newItem: SampleItem) =
oldItem.identifier == newItem.identifier
override fun getChangePayload(
oldItem: SampleItem,
oldItemPosition: Int,
newItem: SampleItem,
newItemPosition: Int
): Any? = createPayload(oldItem.markableItem, newItem.markableItem)
private fun createPayload(
oldItem: MarkableItem,
newItem: MarkableItem
) = when {
! oldItem.isLiked && newItem.isLiked -> LIKE_ITEM
oldItem.isLiked && ! newItem.isLiked -> UNLIKE_ITEM
else -> null
}
}
That is how I define a ViewHolder using the FastAdapter library:
class SampleItem(
val markableItem: MarkableItem,
private val onClickItem: (Item) -> Unit
) : AbstractBindingItem<ItemSampleBinding>() {
override val type: Int = R.layout.item
override var identifier: Long = markableItem.item.hashCode().toLong()
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) =
ItemSampleBinding.inflate(inflater, parent, false)
override fun bindView(binding: ItemSampleBinding, payloads: List<Any>) {
super.bindView(binding, payloads)
with(binding) {
itemName.text = markableItem.item.name
itemImage.setContent(markableItem.item, IMAGE_SIZE)
likeAnimation.progress = if(markableItem.isClicked) 1f else 0f
root.setThrottleClickListener { onClickItem(markableItem.item) }
}
}
}
UPD: The liking animation's duration is 2 seconds.
Does anybody know if there is any way to fix it?

Strange behaviour happen in onBindViewHolder android kotlin

Hey I am working in android kotlin. I am working in reyclerview. I want to do single selection in my items. I tried some code which is working fine.
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
private var selectedItemPosition: Int = 0
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
selectedItemPosition = holder.bindingAdapterPosition
notifyAdapter()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
holder.binding.root.background =
ContextCompat.getDrawable(holder.binding.root.context, drawableColor)
holder.bindItem(getItem(position), position)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(item: ProductVariant?, position: Int) {
}
}
Video for single click is working fine.
When I move onBindViewHolder code inside bindItem it not working. Can someone guide me why this is happening.
vs
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.bindItem(getItem(position), position ,::notifyAdapter)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
private var selectedItemPosition: Int = 0
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(
item: ProductVariant?,
position: Int,
onAdapterChange: () -> Unit
) {
binding.root.setOnClickListener {
selectedItemPosition = position
onAdapterChange()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
binding.root.background =
ContextCompat.getDrawable(binding.root.context, drawableColor)
}
}
The video for single click is not working.
You moved the selectedItemPosition into the ViewHolder class, so you have a separate copy of this property for every instance of the ViewHolder class so you are no longer affecting other items in the list when any one list item is clicked.
This would be much easier to implement by making the view holder class an inner class of the Adapter so it can directly modify the adapter’s selectedItemPosition property. And I would give the property a custom setter so you can automatically notify the adapter of the change instead of having to work with a separate function call. You can also make it notify the adapter specifically of which two row items changed instead of the whole data set (more efficient and by doing it in the property setter you have access to the old and new row positions in one place easily—field and value).
private var selectedItemPosition: Int = 0
set(value) {
val oldPos = field
field = value
notifyItemChanged(oldPos)
notifyItemChanged(value)
}
Even though you are passing the method reference notifyAdapter() in bindItem() function you are not calling it in the OnClickListener that is why it is not working.

item are updating while scrolling up reyclerview android?

i am making an app in which i have to insert data into reyclerview , the recyclerview is working fine but the problem is that when i scroll it up adapter reupdate the data , so to solve this issue the code looks fine but still getting this issue ....................................................
BaseClass
abstract class MultiViewModelBaseAdapter<M : Model, VDB : ViewDataBinding>(private var diffCallback: DiffUtil.ItemCallback<M>) : ListAdapter<M ,BaseViewHolder<VDB>>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<VDB> {
val inflator = LayoutInflater.from(parent.context)
val binding = createBinding(viewType, inflator, parent)
return BaseViewHolder(binding)
}
abstract fun createBinding(viewType: Int, inflater: LayoutInflater, parent: ViewGroup) : VDB
override fun onBindViewHolder(holder: BaseViewHolder<VDB>, position: Int) {
bind(holder.mBinding, getItem(position), position)
holder.mBinding.executePendingBindings()
}
abstract fun bind(binding: VDB, item: M, position: Int)
abstract fun onDataChanged(values: Boolean)}
}
Adapter
class LanguageAdapter(
private val context: Context,
private val mViewModel: LanguageListViewModel,
private val onClickListener: OnItemClickListener<String>
) : MultiViewModelBaseAdapter<LanguageSupportModel, ViewDataBinding>(diffCallback) {
companion object {
private val ADS = 1
private val LANGUAGES = 2
val diffCallback = object : DiffUtil.ItemCallback<LanguageSupportModel>() {
override fun areItemsTheSame(
oldItem: LanguageSupportModel,
newItem: LanguageSupportModel
): Boolean = oldItem.dataId == newItem.dataId
/**
* Note that in kotlin, == checking on data classes compares all contents, but in Java,
* typically you'll implement Object#equals, and use it to compare object contents.
*/
override fun areContentsTheSame(
oldItem: LanguageSupportModel,
newItem: LanguageSupportModel
): Boolean = oldItem == newItem
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
override fun createBinding(viewType: Int, inflater: LayoutInflater, parent: ViewGroup): ViewDataBinding {
return DataBindingUtil.inflate(inflater, R.layout.language_view, parent, false)
}
override fun bind(binding: ViewDataBinding, item: LanguageSupportModel, position: Int) {
binding as LanguageViewDataBinding
binding.apply {
language = item
//click
}
}
override fun onDataChanged(values: Boolean) {}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val languageAdapter = LanguageAdapter(requireContext(), mViewModel, this ,lifecycleScope)
languageAdapter.submitList(LanguageArray.arrayValues())
reyclerview.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = languageAdapter
}
If You will Use Paging concept in Scrolling then it will Solve.
In the JAVA or KOTLIN We can implement.. like..
import androidx.viewpager.widget.PagerAdapter;
Then extends it into Adapter Class.
public void addList(List<ClsList> list) {
this.mResources = list;
notifyDataSetChanged(); // also this main line
}
Problem Solve.
☻♥ Have Fun..

RecyclerView only displaying data after restarting Activity

So i have this strange behaviour where my RecyclerView only Displays data when i start the App and then restart the activity via Android Studio. Or make a change in XML, then undo it and restart.
The then displayed data is correct and also updates when i add things, but it is not visible after starting the app.
Can anyone help me out? I am really clueless why that would be.
Fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false /// We dont need any interaction with Drag&Drop we only want swipe left&right
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val receipt = exampleAdapter.currentList[viewHolder.adapterPosition]
viewModel.onSwipe(receipt)
}
}).attachToRecyclerView(recyclerView)
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted { //as soon as we close our app the events will be suspended, but not deleted and will remain after restart
viewModel.addTaskEvent.collect { event->
when(event){
is PurchaseViewmodel.TasksEvent.ShowUndoDelete -> {
Snackbar.make(requireView(),"Tasks deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.unDoDeleteClick(event.receipts)
}.show()
}
}
}
}
}
}
Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
You should compare corresponding fields of old/new Receipts in areContentsTheSame() instead of comparing the entire old/new objects
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.store == newItem.store
&& oldItem.total == newItem.total
&& oldItem.date == newItem.date

Recycler view items disappear when I scroll my recycler view to top

The problem is that when I scroll up my recycler view some items gets disappear
Here is MyAdapter Code of RecyclerView
class MessageAdapter:androidx.recyclerview.widget.ListAdapter<MessageEntity,MessageAdapter.MessageViewHolder>(MessageDiffCallBack()){
inner class MessageViewHolder(val view: View):RecyclerView.ViewHolder(view)
class MessageDiffCallBack:DiffUtil.ItemCallback<MessageEntity>(){
override fun areItemsTheSame(oldItem: MessageEntity, newItem: MessageEntity): Boolean {
return oldItem.timeStamp==newItem.timeStamp
}
override fun areContentsTheSame(oldItem: MessageEntity, newItem: MessageEntity): Boolean {
return oldItem==newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
val view=LayoutInflater.from(parent.context).inflate(R.layout.chat_item,parent,false)
return MessageViewHolder(view)
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val messageEntity=getItem(position)
val ourTextView=holder.view.findViewById<TextView>(R.id.ourUserMessage)
val otherTextView=holder.view.findViewById<TextView>(R.id.otherUserMessage)
val ourCardView=holder.view.findViewById<CardView>(R.id.ourCardView)
val otherCardView=holder.view.findViewById<CardView>(R.id.otherCardView)
if(messageEntity.senderId==FirebaseAuth.getInstance().currentUser?.uid){
otherCardView.visibility=View.GONE
ourTextView.text=messageEntity.message
}
else{
ourCardView.visibility=View.GONE
otherTextView.text=messageEntity.message
}
}
}
Can anyone help me in this problem

Categories

Resources