RecyclerView list item view not updating (using DiffUtil.ItemCallback) - android

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
}
}

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
}
}

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()
}
}

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)

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
}
}

Your issue: the ViewHolder is not updated when you update the data
There are three ways to trigger a ViewHolder to reload the content:
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
when you notify the adapter that an item changed
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.

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.

Related

DiffUtil Not working with ListAdpater when view is updating Android Kotlin

Hey I have Reyclerview with DiffUtill using ListAdapter. I added element through submitList function. But when updating the list view is not redrawing the element. Until I used notifyDataSetChanged() or setting adapter again. So what the use case of DiffUtill?. What is the proper way of doing to redraw item when item is updated in list as well as in reyclerview.
MainActivity
class MainActivity : AppCompatActivity() {
private var list = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
private lateinit var binding: ActivityMainBinding
var i = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Log.e("List", " $list")
val intAdapter = IntAdapter()
binding.recylerview.adapter = intAdapter
intAdapter.submitList(list)
binding.button.setOnClickListener {
list.add(++i)
intAdapter.submitList(list)
// binding.recylerview.adapter = intAdapter
// intAdapter.notifyDataSetChanged()
}
}
}
IntAdapter
class IntAdapter : ListAdapter<Int, IntViewHolder>(comparator) {
companion object {
private val comparator = object : DiffUtil.ItemCallback<Int>() {
override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntViewHolder {
return IntViewHolder(
IntLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: IntViewHolder, position: Int) {
holder.bindItem(getItem(position))
}
}
IntViewHolder
class IntViewHolder(val binding: IntLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindItem(item: Int?) {
binding.intNumber.text = item.toString()
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/recylerview"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
int_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/intNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
You need to ensure you pass a different instance of List each time you call submitList. If you pass a List, mutate it, and then pass that same List instance again, DiffUtil is only comparing the same list to itself, so it will assume nothing has changed in the list and won't update anything. It doesn't have some sort of memory of what the List contained back when you first submitted it.
To generalize further, you must not use a mutable List with ListAdapter at all. ListAdapter assumes the list you pass to submitList does not change. If you mutate it, there can be unexpected bugs.
Two ways to resolve this in your code.
Create a read-only copy of the list each time you pass it:
intAdapter.submitList(list.toList())
Don't use a MutableList at all. Create a new List every time you modify what should be in the List. This is the simpler, less error-prone solution.
class MainActivity : AppCompatActivity() {
private var list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
private lateinit var binding: ActivityMainBinding
var i = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Log.e("List", " $list")
val intAdapter = IntAdapter()
binding.recylerview.adapter = intAdapter
intAdapter.submitList(list)
binding.button.setOnClickListener {
list += ++i // create a new list from old list contents plus a new item
intAdapter.submitList(list)
}
}
}
Side note: when you have var combined with Mutable____ that should be kind of a red flag. It should be rare that you need two different ways to change something. That is error-prone.
You need to submit a new List, currently you are submitting that same List which has been mutated.
Inside your click listener try something like :
binding.button.setOnClickListener {
val current = intAdapter.currentList
val update = current + (current.size + 1) // returns a new list with added value
intAdapter.submitList(update)
}
Example with this logic :

Creating recycler view item animation

I am trying to creating a animation in a recyclerview. The kind of animation that I am looking for is the one which is available in Telegram android app.
When you open a chat in telegram and long press on the message, the recyclerview multi select option comes with checkbox. I am trying to create the same effect.
My current status:
ListItemAdapter.kt
class ListItemAdapter(private val values: List<PlaceholderContent.PlaceholderItem>
) : RecyclerView.Adapter<ListItemAdapter.ItemViewHolder>() {
private lateinit var itemClick: OnItemClick
private var selectedIndex: Int = -1
private var selectedItems: SparseBooleanArray = SparseBooleanArray()
private var isActive: Boolean = false
fun setItemClick(itemClick: OnItemClick) {
this.itemClick = itemClick
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ListItemAdapter.ItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.fragment_item,
parent,
false
)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ListItemAdapter.ItemViewHolder, position: Int) {
holder.itemView.apply {
findViewById<TextView>(R.id.item_number).text = values[position].id
findViewById<TextView>(R.id.content).text = values[position].content
}
holder.itemView.setOnClickListener {
itemClick.onItemClick(values[position], position)
}
holder.itemView.setOnLongClickListener {
itemClick.onLongPress(values[position], position)
true
}
toggleIcon(holder, position)
}
override fun getItemCount(): Int {
return values.size
}
fun toggleIcon(holder: ItemViewHolder, position: Int){
val checkBox = holder.itemView.findViewById<RadioButton>(R.id.is_selected)
if(selectedItems.get(position, false)){
checkBox.isGone = false
checkBox.isChecked = true
}
else{
checkBox.isGone = true
checkBox.isChecked = false
}
if(isActive) checkBox.isGone = false
if(selectedIndex == position) selectedIndex = - 1
}
fun selectedItemCount() = selectedItems.size()
fun toggleSelection(position: Int){
selectedIndex = position
if (selectedItems.get(position, false)){
selectedItems.delete(position)
}else {
selectedItems.put(position, true)
}
notifyItemChanged(position)
isActive = selectedItems.isNotEmpty()
notifyDataSetChanged()
}
fun clearSelection(){
selectedItems.clear()
notifyDataSetChanged()
}
interface OnItemClick {
fun onItemClick(item: PlaceholderContent.PlaceholderItem, position: Int)
fun onLongPress(item: PlaceholderContent.PlaceholderItem, position: Int)
}
inner class ItemViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
}
}
fragment_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/list_item"
>
<RadioButton
android:id="#+id/is_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="16dp"
/>
<TextView
android:id="#+id/item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="#+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
MainActivity.kt
adapter = ListItemAdapter(PlaceholderContent.ITEMS)
val recyclerViewList = view.findViewById<RecyclerView>(R.id.list)
recyclerViewList.adapter = adapter
recyclerViewList.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
val myHelper = ItemTouchHelper(myCallback)
myHelper.attachToRecyclerView(recyclerViewList)
adapter.setItemClick(object : ListItemAdapter.OnItemClick{
override fun onItemClick(
item: PlaceholderContent.PlaceholderItem,
position: Int
) {
if(adapter.selectedItemCount() > 0)
toggleSelection(position)
}
override fun onLongPress(
item: PlaceholderContent.PlaceholderItem,
position: Int
) {
toggleSelection(position)
}
})
return view
}
private fun toggleSelection(position: Int){
adapter.toggleSelection(position)
}
To add an animated transition, the simplest way is by adding
animateLayoutChanges="true"
to the individual item root(since that's where you'd like it animated), which in your case would be:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/list_item"
animateLayoutChanges="true" <!-- Around here -->
>
...
</LinearLayout>
To have it look exactly like in telegram, you'd need a bit of a different design to start with, but that's another issue(and a matter of perseverance in a way :P);
Also if you'd like different types of animations, you'd need to go at it with a different way, you could do it by adding animate to the view: ( alpha goes from 0f to 1f usually((invisible-visible)), and you'd need to change the view visibility paremeter if needed to visible/invisible/gone so that it doesn't register click events on that area, depending on your needs. In any case, this should be trial & error to get your desired behavior)
is_selected.animate().alpha(1f).setInterpolator(AccelerateDecelerateInterpolator())
is_selected.visibility = View.VISIBLE
You could also read up a little on available android interpolators, since we can't be quite sure whether they will remove some(unlikely)/add new ones.

How to stop a view in RecyclerView from updating its value when it has focus?

I have a RecyclerView with a set of CardView that contain an EditText in each card. The RecyclerView adapter receives a list of numbers every second and it binds the data to each CardView's EditText.
However, when the user click on one of the EditView I want that EditView to stop changing every second while the EditView has the focus, so the user can enter their own data.
I don't really know what to do if (editView.hasFocus)
Adapter
class ConverterRecyclerViewAdapter(val ratesList: ArrayList<Currency>,
val context: Context?,
val onRateListener: OnRateListener)
: RecyclerView.Adapter<ConverterRecyclerViewAdapter.ViewHolder>() {
class ViewHolder(itemView: View, val onRateListener: OnRateListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener{
var editText: EditText
init{
itemView.setOnClickListener(this)
editText = itemView.findViewById(R.id.value_conv_cv)
}
fun bindItem(currency : Currency){
itemView.currency_name_conv_cv.text = currency.currencyName
itemView.country_currency_name_conv_cv.text = currency.countryCurrencyName
itemView.value_conv_cv.text = SpannableStringBuilder(currency.value.toString())
itemView.flag_iv_conv_cv.setImageResource(currency.flag)
itemView.tag = currency.currencyName
}
override fun onClick(v: View?) {
onRateListener.onRateClick(adapterPosition, itemView.currency_name_conv_cv.text.toString())
}
}
interface OnRateListener {
fun onRateClick(position : Int, currency : String)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.converter_card_layout, parent, false)
return ViewHolder(view, onRateListener)
}
override fun getItemCount(): Int {
return ratesList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindItem(ratesList[position])
}
}
CardView.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardElevation="0dp"
app:cardUseCompatPadding="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="#+id/flag_iv_conv_cv"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="#mipmap/ic_launcher_round"/>
<TextView
android:id="#+id/currency_name_conv_cv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CURRENCY"
android:textStyle="bold"
android:textSize="16sp"
android:paddingTop="#dimen/list_item_tv_padding"
android:paddingStart="#dimen/list_item_tv_padding"
android:layout_toRightOf="#id/flag_iv_conv_cv"
android:layout_toEndOf="#+id/flag_iv_conv_cv"
android:textColor="#android:color/black"/>
<TextView
android:id="#+id/country_currency_name_conv_cv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/flag_iv_conv_cv"
android:layout_toEndOf="#+id/flag_iv_conv_cv"
android:layout_below="#+id/currency_name_conv_cv"
android:paddingStart="#dimen/list_item_tv_padding"
android:text="Country Currency Name"
android:textSize="12sp"
android:textColor="#android:color/darker_gray"/>
<EditText
android:id="#+id/value_conv_cv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignEnd="#id/country_currency_name_conv_cv"
android:gravity="right"
android:text="100"
android:textSize="24sp" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(ConverterViewModel::class.java)
viewModel.initialisePositions()
viewModel.setupTimer(viewModel.ratesPositions[0])
val ratesLiveData = viewModel.rates
ratesLiveData.observe(viewLifecycleOwner, Observer { data ->
refreshData(data.rates)
viewModel.sortListByPositionsArray(ratesArray)
val firstElementInRecyclerView = viewModel.ratesPositions[0]
val view = recyclerView.findViewWithTag<View>(firstElementInRecyclerView)
lateinit var editText : EditText
if (converterAdapter == null) setUpRecyclerView()
else {
converterAdapter!!.notifyDataSetChanged()
}
})
}
First of all, you want to update your item when you don't have the focus on it, so indeed you have to use if (!editView.hasFocus()) {}
Then if you have to check if your list ratesArray is holding the same items or not, because if you do ratesArray.clear() at some point, you're clearing the objects then the RecyclerView will create a new ViewHolder for your item.
If you are clearing and creating new objects continuously, you have to "pair" your Adapter with the items.
For example, if you always clear the items and create them again respecting the position, add this method to your Adapter:
override fun getItemId(position: Int): Long {
return position.toLong()
}
you can relate the "rate" in any other ways (a String inside the object, for example). If you do it with the position, you're pairing each item with their order.
Now, to finish "respecting that order", you have to tell the adapter that it has stable ids. That's done with Adapter.setHasStableIds(true)
Maybe like this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if(!holder.editText.hasFocus()){
holder.bindItem(ratesList[position])
}
}
UPDATE
remove the focusable attribute for edittext and add this to its xml
android:focusableInTouchMode="false"
in the adapter:
//decalare clicked item as member of the class and set it to -1
var clickedItem = -1;
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if(clickedItem != position){
holder.bindItem(ratesList[position])
}
//on click
holder.editText.setOnClickListener {
clickedItem = position;
notifyDataSetChanged();
}
}
Your EditText needs special binding so remove this line from fun bindItem():
itemView.value_conv_cv.text = SpannableStringBuilder(currency.value.toString())
And on onBindViewHolder, bind this view only when it's not focused, which requires listening. Try this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val current = ratesList[position]
holder.bindItem(current)
holder.value_conv_cv.setOnFocusChangeListener { view, hasFocus ->
if (hasFocus) {
// do nothing
} else {
holder.value_conv_cv.text = SpannableStringBuilder(current.value.toString())
}
}
}

Why my pagedList is not updated via LiveData method .observe?

I have a Dao with this method.
#Query("SELECT * FROM expense WHERE date BETWEEN :dateStart AND :dateEnd")
fun getExpensesBetweenTheDate(dateStart: Calendar, dateEnd: Calendar):
DataSource.Factory<Int, Expense>
My repository get Dao and create LiveData> object.
fun getExpensesBetweenTheDate(startDay: Calendar, endDay: Calendar): LiveData<PagedList<Expense>> {
val factory = expenseDao.getExpensesBetweenTheDate(startDay, endDay)
val config = PagedList.Config.Builder()
.setPageSize(30)
.setMaxSize(200)
.setEnablePlaceholders(true)
.build()
return LivePagedListBuilder(factory, config)
.build()
}
My ViewModel get repository and create a variable.
val expenses = repository.getExpensesBetweenTheDate(startCalendar, endCalendar)
Finally, MainActivity observes on LiveData.
viewModel.expenses.observe(this, Observer(simpleExpenseAdapter::submitList))
All working fine, but when I try to add a new record to the database, it appears there not immediately, but after restarting the application. Similar code without a paging library works well. Maybe i do something wrong. Just in case, I give below the code of the adapter, viewHolder and layout.
Adapter.
class ExpenseAdapter : PagedListAdapter<Expense, ExpenseViewHolder>(EXPENSE_COMPARATOR) {
companion object {
private val EXPENSE_COMPARATOR = object : DiffUtil.ItemCallback<Expense>() {
override fun areItemsTheSame(oldItem: Expense, newItem: Expense): Boolean {
return oldItem.expenseId == newItem.expenseId
}
override fun areContentsTheSame(oldItem: Expense, newItem: Expense): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpenseViewHolder {
return ExpenseViewHolder.create(parent)
}
override fun onBindViewHolder(holder: ExpenseViewHolder, position: Int) {
val expenseItem = getItem(position)
if (expenseItem != null) holder.bind(expenseItem)
}
}
ViewHolder.
class ExpenseViewHolder(binding: ExpenseElementSimpleBinding) : RecyclerView.ViewHolder(binding.root) {
private val mBinding = binding
init {
mBinding.root.setOnClickListener {
val intent = Intent(it.context, ShowExpenseActivity::class.java)
intent.putExtra("expense", mBinding.expense)
it.context.startActivity(intent)
}
}
companion object {
fun create(parent: ViewGroup): ExpenseViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ExpenseElementSimpleBinding.inflate(inflater, parent, false)
return ExpenseViewHolder(binding)
}
}
fun bind(item: Expense) {
mBinding.apply {
expense = item
executePendingBindings()
}
}
}
Layout.
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="expense"
type="com.example.budgetplanning.data.model.Expense"/>
</data>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:text="#{expense.description}"
tools:text="Gasoline"
android:padding="5dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<androidx.appcompat.widget.AppCompatTextView
android:text="#{String.valueOf(expense.amount)}"
tools:text="123"
android:padding="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
You have to call the simpleExpenseAdapter.notifyDataSetChanged() after the the submitList
This is happening because when you are calling simpleExpenseAdapter::submitList is equivalent to call simpleExpenseAdapter:submitList() when the list diff is not called at this time. So, you have to notify that the list has changed.
Or so, you can pass the new list as a parameter like:
viewModel.expenses.observe(this, Observer<YourObjectListened> {
simpleExpenseAdapter.submitList(it)
})
try to use toLiveData with original example from Paging library overview

Is SwipeRefreshLayout compatible with an adapter that has multiple viewType?

I am making a list, that will get more items when I go to the bottom of the list. I want this list to be refreshable too, so I need a SwipeRefreshLayout.
Let's say I have an adapter like this :
class OrdersListAdapter(val selected : (Order) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun addOrders(orders: ArrayList<Order>) {
this.orders.addAll(orders)
notifyItemRangeInserted(this.orders.size - orders.size + FIRST_ITEM_INDEX, orders.size)
}
fun setFirstOrders(orders: ArrayList<Order>) {
this.orders = orders
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return (orders.size + 1 + FIRST_ITEM_INDEX)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_ORDERS -> OrdersViewHolder(DataBindingUtil.inflate<OrdersItemBinding>(LayoutInflater.from(parent.context),
R.layout.orders_item, parent, false).apply {
viewModel = OrdersViewModel()
})
else -> LoadingMoreOrdersViewHolder(DataBindingUtil.inflate<LoadingMoreOrdersItemBinding>(LayoutInflater.from(parent.context),
R.layout.loading_more_orders_item, parent, false).apply {
viewModel = LoadingMoreOrdersViewModel()
})
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
TYPE_ORDERS -> (holder as? OrdersViewHolder)?.setData(orders[position - FIRST_ITEM_INDEX])
TYPE_LOADING -> (holder as? LoadingMoreOrdersViewHolder)?.setState(loading, error, noMoreItems)
}
}
override fun getItemViewType(position: Int): Int =
if (position == FIRST_ITEM_INDEX - 1) TYPE_HEADER else if (position == itemCount - 1) TYPE_LOADING else TYPE_ORDERS
Well if I put my RecyclerView like this, inside a SwipeRefreshLayout :
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar">
<android.support.v7.widget.RecyclerView
android:id="#+id/orders_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
</android.support.v4.widget.SwipeRefreshLayout>
This will display all items, but the refreshLayout will not work : If I pull my view, nothing happens.
But if I do not set any loading layout inside my adapter (the one at bottom of the list, to load more items, inflated from R.layout.loading_more_orders_item), then the SwipeRefreshLayout will work properly.
How can I have 2 view types in my adapter, AND set my RecyclerView in a SwipeRefreshLayout ?
EDIT :
Here is my onRefreshListener :
refreshLayout = binding.swipeRefreshLayout
refreshLayout?.setOnRefreshListener { reloadOrders() }
And in reloadOrders() :
private fun reloadOrders() {
viewModel.setLoading()
refreshLayout?.isRefreshing = false
Observable.timer(1, TimeUnit.SECONDS)
.subscribe { getOrders() }
}
I believe that what happens is the recyclerview takes over the ontouch events from swiperefreshlayout. You could try overwriting linearlayout and its canScrollVertically() method to return false. Cheers!
Answering your question, compatibility is not the issue you're having. Not sure of the size of your layout, as its height is 0dp. I'd play around with the view sizes and view hierarchy.
Okay so thanks to #r2rek, I found a solution that worked for me :
In the code, we create a class that extends LinearLayoutManager and overrides method canScrollVertically() to always return false :
inner class MyLinearLayout(context: Context) : LinearLayoutManager(context) {
override fun canScrollVertically(): Boolean {
return (false)
}
}
Then, when we init our RecyclerView, we apply this class to our layoutManager :
ordersRv?.layoutManager = MyLinearLayout(this)
This will allow you to refresh, however you will not be able to scroll in your RecyclerView (duh).
To fix this, add a NestedScrollView in your xml, as following :
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:visibility="#{viewModel.ordersVisibility}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/orders_rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.NestedScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
You can now scroll in your RecyclerView, and pull to refresh with your SwipeRefreshLayout.
I think this method is kinda hacky, so if someone have a better solution, feel free to tell us !

Categories

Resources