1

SOLVED

RV - recycler view

I have an RV inside an alertdialog. Adapter for the RV extends ListAdapter with DiffUtil.ItemCallback. List for the adapter is being updated every 500ms using countdowntimer (checking whether the list item is downloaded or not).

The problem is, the list is updated and submitted to the adapter with the new data and but the list item view is not updating based on new data provided as shown below. I'm using data/view binding for updating the list item view.

The RV sometimes updates the item view when being scrolled.

PS: The RV is a child of NestedScrollView

This is how it is working right now

Adapter code

class AlarmSongsAdapter(
    private val onItemClicked: (AlarmSongItem) -> Unit,
    private val startDownloading: (String) -> Unit,
    private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : ListAdapter<AlarmSongItem, AlarmSongsAdapter.AlarmSongsViewHolder>(DiffUtilCallback) {

object DiffUtilCallback : DiffUtil.ItemCallback<AlarmSongItem>() {
    override fun areItemsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
        return oldItem.id == newItem.id
    }

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

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmSongsViewHolder {
    return AlarmSongsViewHolder(AlarmsSongListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), onItemClicked, startDownloading, insertDownloadEntityInDB)
}

override fun onBindViewHolder(holder: AlarmSongsViewHolder, position: Int) {
    holder.bind(getItem(position))
}

class AlarmSongsViewHolder(
    private val binding: AlarmsSongListItemBinding,
    private val onItemClicked: (AlarmSongItem) -> Unit,
    private val startDownloading: (String) -> Unit,
    private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
    fun bind(alarmSongItem: AlarmSongItem) {
        binding.alarmSongItem = alarmSongItem
        binding.executePendingBindings()
    }

    init {
        binding.downloadButton.setOnClickListener {
            val alarmSongItem = binding.alarmSongItem!!
            when(alarmSongItem.downloadState){
                Download.STATE_STOPPED -> {
                    startDownloading(alarmSongItem.audioFile)
                    val storageInfo = StorageUtils.currentStorageTypeAndPath(binding.root.context)
                    insertDownloadEntityInDB(alarmSongItem.toDownloadEntity(storageInfo))
                }
                else -> {}
            }
        }

        binding.root.setOnClickListener {
            onItemClicked(binding.alarmSongItem!!)
        }
    }
}
}

List item view code

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
    <variable
        name="alarmSongItem"
        type="com.baja.app.domain.models.AlarmSongItem" />
</data>

<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    app:cardElevation="5dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp">

        <androidx.cardview.widget.CardView
            android:id="@+id/song_item_thumbnail_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:cardBackgroundColor="@android:color/transparent"
            app:cardCornerRadius="6dp"
            app:cardElevation="0dp">

            <ImageView
                android:id="@+id/song_item_thumbnail"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_centerVertical="true"
                android:scaleType="centerCrop"
                app:srcCompat="@drawable/bg_default_light"
                tools:ignore="ContentDescription"
                app:thumbnailFromUri="@{alarmSongItem.thumbnail}" />

        </androidx.cardview.widget.CardView>

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="60dp"
            android:id="@+id/download_progress_container"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true">

            <ImageView
                android:id="@+id/download_bg"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:scaleType="centerCrop"
                app:srcCompat="?bg_default_circular"
                tools:ignore="ContentDescription"
                android:layout_centerInParent="true" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/download_button"
                style="@style/AppTheme.OutlinedButton.Icon"
                android:layout_width="32dp"
                android:layout_height="32dp"
                app:cornerRadius="32dp"
                app:icon="@drawable/ic_download"
                app:iconTint="@android:color/white"
                changeIcon="@{alarmSongItem.downloadState}"
                android:layout_centerInParent="true" />

            <com.google.android.material.progressindicator.ProgressIndicator
                android:id="@+id/download_progress_bar"
                style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
                android:layout_width="33dp"
                android:layout_height="33dp"
                app:circularRadius="17dp"
                app:indicatorColor="?attr/progressIndicatorColor"
                app:indicatorWidth="1dp"
                showProgressBar="@{alarmSongItem.downloadState}"
                android:layout_centerInParent="true"
                android:visibility="gone" />

        </RelativeLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_marginStart="20dp"
            android:layout_toEndOf="@id/song_item_thumbnail_container"
            android:orientation="vertical"
            android:weightSum="2"
            android:layout_toStartOf="@id/download_progress_container"
            android:layout_marginEnd="8dp">

            <TextView
                android:id="@+id/song_item_name"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:ellipsize="end"
                android:gravity="bottom"
                android:maxLines="1"
                android:textSize="16sp"
                android:textStyle="bold"
                tools:text="Sa re ga ma pa"
                android:text="@{alarmSongItem.title}" />


            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/song_item_artist"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_marginEnd="4dp"
                    android:ellipsize="end"
                    android:gravity="center_vertical"
                    android:maxWidth="150dp"
                    android:maxLines="1"
                    android:textSize="14sp"
                    tools:text="Sidharth Arun"
                    android:text="@{alarmSongItem.artist}" />

                <View
                    android:layout_width="5dp"
                    android:layout_height="5dp"
                    android:layout_gravity="center_vertical"
                    android:background="@drawable/dot" />

                <TextView
                    android:id="@+id/song_item_duration"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_marginStart="4dp"
                    android:ellipsize="end"
                    android:gravity="center_vertical"
                    android:maxLines="1"
                    tools:text="10:12"
                    app:formatDuration="@{alarmSongItem.duration}" />

            </LinearLayout>
        </LinearLayout>
    </RelativeLayout>

</com.google.android.material.card.MaterialCardView>

Binding Adapter functions

@BindingAdapter("thumbnailFromUri")
fun thumbnailFromUri(view: ImageView, uri: String) {
    Glide.with(view).load(uri).placeholder(R.drawable.bg_default_light).error(R.drawable.bg_default_light).into(view)
}

@BindingAdapter("changeIcon")
fun changeIconBasedOnDownloadState(view: MaterialButton, state: Int) {
    when (state) {
        Download.STATE_COMPLETED -> view.setIconResource(R.drawable.ic_check)
        else -> view.setIconResource(R.drawable.ic_download)
    }
}

@BindingAdapter("showProgressBar")
fun showProgressbarBasedOnState(view: ProgressIndicator, state: Int) {
    when (state) {
        Download.STATE_QUEUED,
        Download.STATE_RESTARTING,
        Download.STATE_DOWNLOADING -> view.visibility = View.VISIBLE
        else -> view.visibility = View.GONE
    }
}
Ankit Gupta
  • 512
  • 1
  • 6
  • 19
  • Ever tried returning false from areContentsTheSame,areItemsTheSame. I know DiffUtils will lose its purpose but it will refresh the whole list again. – Abdul Nov 22 '20 at 16:38
  • DiffUtil requires a new list to be submitted to compare to the old list, you'll need to include the activity or fragment loads RV adapter to get the proper answer. – Shawn Nov 23 '20 at 04:48
  • Please post the source code of `AlarmSongItem` and also where you collect a list of them to call `submitList`. – aminography Nov 23 '20 at 06:56
  • Please check my answer! – Sarah Khan Nov 23 '20 at 11:50

6 Answers6

2

The video was extremely helpful in pinpointing the probelm.

Your Diffutils "areContentsTheSame()" is checking the item not an individual property of the item. When the file is downloaded you need to have "areContentsTheSame()" check the download property to tell if there was a change in the specific property.

example

class MyDiffCallback : DiffUtil.ItemCallback<Dev>() {
    ... 

    override fun areContentsTheSame(oldItem: Dev, newItem: Dev): Boolean {
        return oldItem.downloadStatus == newItem.download.status && 
        oldItem == newItem
    }
}
Shawn
  • 1,222
  • 1
  • 18
  • 41
  • Hi Shawn, my DiffUtil is working correctly and providing the updated list to recyclerView. My problem is, the view is updated only when scrolled. Kindly checkout the **video** attached. – Ankit Gupta Nov 23 '20 at 06:06
  • @AnkitGupta I've updated my answer, the video was helpful.. – Shawn Nov 23 '20 at 06:52
2

The problem is, the list is updated and submitted to the adapter with the new data and but the list item view is not updating based on new data provided as shown below. I'm using data/view binding for updating the list item view.

This happens because you're submitting the same list to your submitList() you can take a look at this post for more information

I've had the same issue recently and I've been able to resolve it quite easily by using onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList<Any>)

In your DiffUtilCallback:

const val BUNDLE_TIME = "bundle_time"
object DiffUtilCallback : DiffUtil.ItemCallback<AlarmSongItem>() {
    override fun areItemsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean = false

    // This will be called every time you submit a list (so every 500ms)
    override fun getChangePayload(oldItem: AlarmSongItem, newItem: AlarmSongItem): Any {
        val diffBundle = Bundle()

        // pass the data you want to update
        diffBundle.putLong(BUNDLE_TIME, newItem.time)

        return diffBundle
    }
}

and then in your adapter override: note that there is payloads:MutableList at the end

override fun onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList<Any>) {
    if(payloads.isEmpty()) {
        // if empty it's a new item that appears on the screen
        super.onBindViewHolder(holder, position, payloads)
        return
    }
    payloads.forEach { when(it) {
        is Bundle -> {
            val time = it.getLong(BUNDLE_TIME)
            holder.binding.alarmSongItem.time.text = time.toString()
        }
    }}
}

You can even create a function in your ViewHolder to pass the data to update if you don't want to expose binding

class AlarmSongsViewHolder(
    private val binding: AlarmsSongListItemBinding,
    private val onItemClicked: (AlarmSongItem) -> Unit,
    private val startDownloading: (String) -> Unit,
    private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(alarmSongItem: AlarmSongItem) {
        binding.alarmSongItem = alarmSongItem
        binding.executePendingBindings()
    }

    fun updateMyItem(time: Long) {
        binding.alarmSongItem.time.text = time.toString()
    }
}
Biscuit
  • 4,840
  • 4
  • 26
  • 54
0

Remove changeIconBasedOnDownloadState and put the code in bind(). Assuming AlarmSongItem.downloadState has a different value in the new list, this is what you need to do.

Move your Binding Adapter code to bind()

fun bind(alarmSongItem: AlarmSongItem) {
    binding.alarmSongItem = alarmSongItem
    binding.executePendingBindings()

     when (alarmSongItem.downloadState) {
        Download.STATE_QUEUED,
        Download.STATE_RESTARTING,
        Download.STATE_DOWNLOADING -> view.visibility = View.VISIBLE
        else -> view.visibility = View.GONE
    }
}
Biscuit
  • 4,840
  • 4
  • 26
  • 54
  • Please show the code where you update your list on click and submit new list using ```adapter.submitList()``` – Debarshi Bhattacharjee Nov 23 '20 at 20:26
  • The actual update will only work when ```DiffUtil``` compares the **old and new Item(to be specific the last state and new state)** and accordingly update the UI. So you have to make sure that new AlarmSongItem's **downloadState** is different from the last one. That way the comparison in **DiffUtil** will update the UI. – Debarshi Bhattacharjee Nov 23 '20 at 20:31
  • The problem is diffUtil is updating the new list and providing it to the Recyclerview, but the view is not updating, even it has the new data. Kindly check the video provided. – Ankit Gupta Nov 24 '20 at 00:08
  • I have seen the video. It's currently working because when you scroll out and scroll back in, the view gets re-created with the updated state. The same should happen with **DiffUtil**. I had a similar approach in one of my app. **DiffUtil** was able to do so instantly.. Just need to make sure the two states in are different in the new and old list. – Debarshi Bhattacharjee Nov 24 '20 at 21:00
0

Your issue: the ViewHolder is not updated when you update the data

There are three ways to trigger a ViewHolder to reload the content:

  1. you scroll it out of the screen and back in -> since it is a recyclerView it will reuse the ViewHolder for another Item and when you scroll back up, it will reload the first items -> updated the img
  2. when you notify the adapter that an item changed
  3. using DiffUtils and reload the ViewHolder by calling diffResult.dispatchUpdatesTo(adapter)

First options seems to be working for you!

For the second:
If you call adapter.notifyDataSetChanged() it will reload all ViewHolders.
If you call adapter.notifyItemChanged(int position) it will reload a specific item position.

To understand the origin of your issue, you might want to try this to just see whether the issue lies deeper.

For the third:
Please show the code where you calculate the DiffUtil result.

Tobi
  • 858
  • 7
  • 15
  • *notifyDataSetChanged()* will refresh the whole list of views, which I do not want in any case. – Ankit Gupta Nov 22 '20 at 08:09
  • 1
    @AnkitGupta totally understandable! That's why I mentioned the call `adapter.notifyItemChanged(int position)` with which you can just reload a single position! The call `notifyDataSetChanged()` was mainly explained for completeness – Tobi Nov 22 '20 at 10:43
  • I don't see any purpose of using DiffUtils if I've to use notifyitemchanged. – Ankit Gupta Nov 22 '20 at 23:55
  • You don't need to use notifyItemChanged if you are using DiffUtil, as quoted from the documentation. " It can be used to calculate updates for a RecyclerView Adapter. See ListAdapter and AsyncListDiffer which can simplify the use of DiffUtil on a background thread." – Shawn Nov 23 '20 at 04:36
  • @AnkitGupta you are right that DiffUtils, could do the job. But as it seems something is wrong here. Please provide the two Log messages to see that the two methods are returning what you expect. – Tobi Nov 23 '20 at 22:37
  • 1
    @Tobi as I already mentioned, there is no problem with the DiffUtil, the recyclerView and its views has the new data, its just, the view is not updating even after having the new data. – Ankit Gupta Nov 24 '20 at 00:11
  • Ok, so if the DiffUtil are working fine, verify whether you *apply* the diffResult correctly: `diffResult.dispatchUpdatesTo(adapter)`. This call an update of the views for which data has changed. – Tobi Nov 24 '20 at 06:43
  • Hi @AnkitGupta, since I am updating item onItemClick from Adapter, Shall I pass the entire List to call SubmitList again or just the position to call NotifyItemChanged(pos)? I am using ListAdapter and calling submitList(list) is not possible from there unless i pass it as a param with onItemClick – Aveek Jun 02 '21 at 15:09
  • The problem I am facing is the view is not updated once the data is updated. I need to scroll to see the update. @AnkitGupta – Aveek Jun 02 '21 at 15:10
  • @Aveek yes, use submitlist to update the list contents. If you can share your code via codelab or something, that would be more helpful. – Ankit Gupta Jun 02 '21 at 16:49
0

You need to add a method to your Adapter which will be called when list is updated

class AlarmSongsAdapter(alarmSongItems: List<AlarmSongItem>) : RecyclerView.Adapter<AlarmSongsAdapter.ViewHolder>() {

    private val mAlarmSongItems = mutableListOf<AlarmSongItem>()

    init {
        mAlarmSongItems.addAll(alarmSongItems)
    }

    fun swap(alarmSongItems: List<AlarmSongItem>) {
            val diffCallback = DiffUtilCallback(this.mAlarmSongItems, alarmSongItems)
            val diffResult = DiffUtil.calculateDiff(diffCallback)
    
            this.mAlarmSongItems.clear()
            this.mAlarmSongItems.addAll(alarmSongItems)
            diffResult.dispatchUpdatesTo(this)
        }
}

mAlarmSongItems is the initial list you passed to your adapter (Sorry, I didn't copy all your variables, just the one needed to show difference meaningful)

Your callback,

class DiffUtilCallback(
    private val oldList: List<AlarmSongItem>,
    private val newList: List<AlarmSongItem>
) : DiffUtil.Callback() {

    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.get(oldItemPosition).equals(newList.get(newItemPosition));
    }

}

So now wherever your receive your updated countdowntimer values, where you initialized your adapter, you could do

alarmAdapter.swap(updatedAlarmValues)
Sarah Khan
  • 836
  • 7
  • 11
  • 1
    stil doesn't work ,after diffResult.dispatchUpdatesTo(this) the new list is not getting bind to the holder – joghm Feb 27 '21 at 00:30
0

I've solved the issue (in my way). Thanks for all the answers, they are really helpful, but not in my usecase.

Solution:

To make it work as accurately, I created another variable for download details(like state, percentage etc) in my rv_list_item.xml and passed the respective download.

DiffUtil is now working perfectly.

Ankit Gupta
  • 512
  • 1
  • 6
  • 19