I have implemented a simple adapter but it is causing RecyclerView not to recycler views and calls onCreateViewHolder() for every list item when scrolled. This causes jank
whenever I scroll the list. Few points listed below are not related to excessive calls of onCreateViewHolder(), but I tried them to improve scroll performance and avoid jank. Things I have tried so far:
recyclerView.setHasFixedSize(true)
recyclerView.recycledViewPool.setMaxRecycledViews(1, 10) with recyclerView.setItemViewCacheSize(10)
recyclerView.setDrawingCacheEnabled(true) with recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH)
setting RecyclerView height to "match_parent"
Was previously using Kotlin's synthetic, now moved to Android's ViewBinding
Rewrite complex nested layouts to Constraint Layout
override onFailedToRecycleView() to see if it is called, but it was never called
Here is my adapter:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.suppstore.R
import com.example.suppstore.common.Models.Brand
import com.example.suppstore.databinding.LiBrandBinding
import com.google.firebase.perf.metrics.AddTrace
class BrandsAdapter(list: ArrayList<Brand>, var listener: BrandClickListener?) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val VIEW_TYPE_LOADING = 0
private val VIEW_TYPE_NORMAL = 1
private var brandsList: ArrayList<Brand> = list
#AddTrace(name = "Brands - onCreateViewHolder", enabled = true)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == VIEW_TYPE_NORMAL) {
ViewHolder(
LiBrandBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
)
} else {
LoaderHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.li_loader, parent, false)
)
}
}
#AddTrace(name = "Brands - onBindViewHolder", enabled = true)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder)
holder.setData(brandsList[position], listener!!)
}
class ViewHolder(itemView: LiBrandBinding) : RecyclerView.ViewHolder(itemView.root) {
private val binding: LiBrandBinding = itemView
#AddTrace(name = "Brands - ViewHolder-setData", enabled = true)
fun setData(brand: Brand, listener: BrandClickListener) {
binding.cardItem.setOnClickListener { listener.onItemClick(brand) }
binding.tvBrandName.text = brand.name
binding.tvCount.text = brand.count.toString() + " Products"
}
}
class LoaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView.rootView) {
}
#AddTrace(name = "Brands - addLoader", enabled = true)
fun addLoader() {
brandsList.add(Brand())
notifyItemInserted(brandsList.size - 1)
}
#AddTrace(name = "Brands - setData", enabled = true)
fun setData(newList: ArrayList<Brand>) {
this.brandsList = newList
notifyDataSetChanged()
}
#AddTrace(name = "Brands - removeLoader", enabled = true)
fun removeLoader() {
if (brandsList.size == 0)
return
val pos = brandsList.size - 1
brandsList.removeAt(pos)
notifyItemRemoved(pos)
}
override fun getItemViewType(position: Int): Int {
return if (brandsList.get(position).count == -1) {
VIEW_TYPE_LOADING
} else
VIEW_TYPE_NORMAL
}
interface BrandClickListener {
fun onItemClick(brand: Brand)
}
override fun getItemCount(): Int {
return brandsList.size
}
}
Here is the list item (li_brand):
<?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:id="#+id/cardItem"
android:layout_width="match_parent"
android:layout_height="85dp"
android:background="#color/app_black">
<TextView
android:id="#+id/tvBrandName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:textColor="#color/app_yellow"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#id/tvCount"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="#+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="2dp"
android:textColor="#color/app_grey"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvBrandName" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="15dp"
android:src="#drawable/ic_baseline_arrow_forward_ios_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="#color/app_bg"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Here are related functions in Fragment
class BrandsFragment : Fragment() {
private val adapter = BrandsAdapter(ArrayList(), brandClickListener())
fun brandClickListener(): BrandsAdapter.BrandClickListener {
return object : BrandsAdapter.BrandClickListener {
override fun onItemClick(brand: Brand) {
activityViewModel?.setSelectedBrand(brand)
}
}
}
fun setupRecyclerView() {
val llManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.recyclerView.layoutManager = llManager
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { //check for scroll down
val visibleItemCount = llManager.childCount
val totalItemCount = llManager.itemCount
val firstVisibleItemPos = llManager.findFirstVisibleItemPosition()
if (loadWhenScrolled
&& visibleItemCount + firstVisibleItemPos >= totalItemCount
&& firstVisibleItemPos >= 0
) {
//ensures that last item was visible, so fetch next page
loadWhenScrolled = false
viewModel.nextPage()
}
}
}
})
binding.recyclerView.adapter = adapter
}
}
And here is the fragment xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:background="#color/app_black"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".Brands.BrandsFragment">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#drawable/bottom_line_yellow"
android:theme="#style/SearchViewTheme"
app:closeIcon="#drawable/ic_baseline_close_24"
app:iconifiedByDefault="false"
app:queryBackground="#android:color/transparent"
app:queryHint="Atleast 3 characters to search"
app:searchIcon="#drawable/ic_baseline_search_24" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Have you tried RecyclerView Diffutil class? Hope it will resolve smooth scrolling issue and overwhelm recreation of items.
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
"DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one."
Related
enter image description here
When I click on cardview I want the other cardview color to turn white
here design is that i show in recyclerview
<?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="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:backgroundTint="#android:color/transparent"
android:elevation="0dp"
android:paddingBottom="10dp">
<androidx.cardview.widget.CardView
android:id="#+id/mainCard"
android:layout_width="80dp"
android:layout_height="100dp"
android:layout_marginLeft="1dp"
android:layout_marginTop="1dp"
android:layout_marginBottom="10dp"
app:cardCornerRadius="10dp"
app:cardElevation="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/cardImage"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_gravity="center"
android:layout_marginBottom="20dp"
app:srcCompat="#drawable/cate1" />
<TextView
android:id="#+id/mainText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="10dp"
android:text="Telefon, Tablet\nve Aksesuarlar"
android:textColor="#302E2E"
android:textSize="10sp" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="#+id/colorCard"
android:layout_width="82dp"
android:layout_height="102dp"
android:visibility="invisible"
app:cardBackgroundColor="#color/orange"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
The block with adapter codes for the recyclerview that I have listed
I couldn't understand the logic in setOnclickListener
and
I don't know how to make an algorithm
class CategoryMainAdapter(val mContext: Context) :
RecyclerView.Adapter<CategoryMainAdapter.ViewHolderClass>() {
var categoryMainList: List<CategoryMain> = listOf()
inner class ViewHolderClass(view: View) : RecyclerView.ViewHolder(view) {
val mainCard: CardView
val cardImage: ImageView
val mainText: TextView
val colorCard: CardView
init {
mainCard = view.findViewById(R.id.mainCard)
cardImage = view.findViewById(R.id.cardImage)
mainText = view.findViewById(R.id.mainText)
colorCard = view.findViewById(R.id.colorCard)
}
}
#SuppressLint("NotifyDataSetChanged")
fun setList(list: List<CategoryMain>) {
categoryMainList = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderClass {
val design =
LayoutInflater.from(mContext).inflate(R.layout.main_category_design, parent, false)
return ViewHolderClass(design)
}
override fun onBindViewHolder(holder: ViewHolderClass, position: Int) {
val categoryMain = categoryMainList[position]
holder.mainText.text = categoryMain.categoryName
holder.cardImage.setImageResource(categoryMain.categoryPhoto)
holder.mainCard.setOnClickListener() {
holder.colorCard.visibility = View.VISIBLE
holder.mainText.setTextColor(ContextCompat.getColor(mContext, R.color.orange))
}
}
override fun getItemCount(): Int {
return categoryMainList.size
}
}
You can use a multi-select functionality in recycler view like this: How to implement multi-select in RecyclerView? or Multiple selected items RecyclerView in Activity.java
i solved my problem
now when i click on which cardview its color changes
class CategoryMainAdapter(val mContext: Context) :
RecyclerView.Adapter<CategoryMainAdapter.ViewHolderClass>() {
var categoryMainList: List<CategoryMain> = listOf()
var clikedPosition : Int = -1
inner class ViewHolderClass(view: View) : RecyclerView.ViewHolder(view) {
val mainCard: CardView
val cardImage: ImageView
val mainText: TextView
val colorCard: CardView
init {
mainCard = view.findViewById(R.id.mainCard)
cardImage = view.findViewById(R.id.cardImage)
mainText = view.findViewById(R.id.mainText)
colorCard = view.findViewById(R.id.colorCard)
}
}
#SuppressLint("NotifyDataSetChanged")
fun setList(list: List<CategoryMain>) {
categoryMainList = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderClass {
val design =
LayoutInflater.from(mContext).inflate(R.layout.main_category_design, parent, false)
return ViewHolderClass(design)
}
override fun onBindViewHolder(holder: ViewHolderClass, position: Int) {
val categoryMain = categoryMainList[position]
holder.mainText.text = categoryMain.categoryName
holder.cardImage.setImageResource(categoryMain.categoryPhoto)
if (categoryMain.isSelected == true){
categoryMainList[position].isSelected = false
holder.colorCard.visibility = View.INVISIBLE
holder.mainText.setTextColor(ContextCompat.getColor(mContext, R.color.blacklow))
}
holder.mainCard.setOnClickListener() {
if (clikedPosition != -1){
notifyItemChanged(clikedPosition)
}
clikedPosition = position
categoryMainList[position].isSelected = true
holder.colorCard.visibility = View.VISIBLE
holder.mainText.setTextColor(ContextCompat.getColor(mContext, R.color.orange))
}
}
override fun getItemCount(): Int {
return categoryMainList.size
}
}
What I want to do:
My idea is simple:
I have RecyclerView in RelativeLayout
When data loaded I set first item's text to TextView for pinned message
On scroll I take first visible item and set this item's text to header (TextView)
So I have:
RelativeLayout + RecyclerView + TextView for pinned message + TextView for header
I update header on scroll, it looks like sticky header for list.
It is my layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<TextView
android:id="#+id/pin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ebebeb"
android:gravity="center"
android:padding="24dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/pin"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#id/list"
android:layout_alignEnd="#id/list"
android:background="#33ff0000"
android:gravity="center"
android:padding="24dp"/>
</RelativeLayout>
And it is my Activity:
class CustomAdapter() :
RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
var data: List<UUID> = listOf()
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView
init {
textView = view.findViewById(R.id.text)
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.list_item, viewGroup, false)
return ViewHolder(view)
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
viewHolder.textView.text = data[position].toString()
}
override fun getItemCount() = data.size
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = CustomAdapter()
val header = findViewById<TextView>(R.id.header)
findViewById<RecyclerView>(R.id.list).apply {
this.adapter = adapter
layoutManager = LinearLayoutManager(this#MainActivity, LinearLayoutManager.VERTICAL, false)
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val position = recyclerView.topChildPosition() ?: return
if (position >= 0) {
header.text = adapter.data[position].toString().substring(0, 10)
Log.w("MainActivity", header.text.toString())
} else {
header.text = null
}
}
})
}
/**
* SIMULATE DATA LOADING
*/
Handler(Looper.getMainLooper()).postDelayed({
adapter.data = List(100) {
UUID.randomUUID()
}
findViewById<TextView>(R.id.pin).text = adapter.data[0].toString()
adapter.notifyDataSetChanged()
}, 1000L)
}
private fun RecyclerView.topChildPosition(): Int? {
layoutManager.let { layoutManager ->
if (layoutManager != null && layoutManager is LinearLayoutManager) {
return if (!layoutManager.reverseLayout) layoutManager.findFirstVisibleItemPosition()
else layoutManager.findLastVisibleItemPosition()
} else {
val topChild: View = getChildAt(0) ?: return null
return getChildAdapterPosition(topChild)
}
}
}
}
And what I have:
RelativeLayout measured views, header has text = null, so header's width = padding only
Data loaded (see "SIMULATE DATA LOADING" in the activity code), I call notifyDataSetChanged and I set text to header. Header (TextView) calls requestLayout() on set text, but RelativeLayout doesn't measured new width (actually, RelativeLayout calls header's onMeasure with widthMode == MeasureSpec.EXACTLY and pass old width)
When I scroll RecyclerView header remeasured successfully.
I can reproduce it on Android 26-33
What I can do with this.
I can change layout:
from this:
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#id/list"
android:layout_alignEnd="#id/list"
android:background="#33ff0000"
android:gravity="center"
android:padding="24dp"/>
to this:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignTop="#id/list"
android:layout_alignEnd="#id/list">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="#33ff0000"
android:gravity="center"
android:padding="24dp"/>
</FrameLayout>
But actually I can't, because really my TextView in custom view extends from TextView and it is part of library. I don't know how it will be used in layouts.
I can call requestLayout() after data loading like this:
adapter.notifyDataSetChanged()
header.doOnNextLayout { header.post { header.requestLayout() } }
But it is ugly.
So, finally my questions:
Do you know how to fix it without layout changing?
Do you know bug or some documented RelativeLayout behavior
Thanks for any help!
I am trying to create a recycler listview that houses integer numbers between 1 and 250.
The issue I am having is that when I scroll through the list, it only displays 1-9 and then randomly shows only single digits. Is it recycling the cached item values?
Here is how my adapter looks:
package com.work.me
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.counter_layout.view.*
class CounterAdapter : RecyclerView.Adapter<CounterAdapter.ViewHolder>() {
private val counterList = mutableListOf<Int>()
init {
for (x in 0..COUNTER_MAX) {
Log.d("JJJ", "x is $x")
counterList.add(x + COUNTER_OFFSET)
}
}
inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bindView(counterValue: String) {
Log.d("JJJ", "counterValue is $counterValue")
view.counterText.text = counterValue
Log.d("JJJ", " view.counterText is ${view.counterText.text}")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.counter_layout, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
Log.d("JJJ", "size is " + counterList.size)
return counterList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(counterList[position + COUNTER_OFFSET].toString())
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
companion object {
const val COUNTER_MAX = 250
const val COUNTER_OFFSET = 1
}
}
I have placed logs as you can see and on the bindView function, the value passed is correct but never displayed on the actuial list ui widget.
Here is how i initiate the list widget with the adapter:
counterList.apply {
adapter = counterAdapter
// setHasFixedSize(true)
layoutManager = LinearLayoutManager(this#MainActivity)
}
Here is the counter layout of each item:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="#+id/counterText"
style="#style/TextAppearance.AppCompat.Headline" />
</androidx.constraintlayout.widget.ConstraintLayout>
Activty layout
<?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:id="#+id/counterList"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#id/counterButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
Thanks in advance
I think you have the old view reference in the holder.
Try to get rid of it and use itemView instead
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView(counterValue: String) {
Log.d("JJJ", "counterValue is $counterValue")
itemView.counterText.text = counterValue
Log.d("JJJ", " view.counterText is ${itemView.counterText.text}")
}
}
I have created a recycle view and inside that using card view for items. I have a delete button inside a card view whenever I click on that button my item is deleted from SQLite database. But to reflect it on UI, app need to restart. How can I notify adpater that item is deleted?
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">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="57dp"
android:layout_height="64dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="40dp"
android:clickable="true"
android:onClick="addNewCredentials"
app:backgroundTint="#270867"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="#android:drawable/ic_menu_add" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="1dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="1dp"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" >
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
list_item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/urlView"
android:layout_width="300dp"
android:layout_height="30dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="url"
android:textAppearance="#style/TextAppearance.AppCompat.Large"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#+id/userNameView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="#+id/userNameView"
android:layout_width="300dp"
android:layout_height="25dp"
android:layout_marginBottom="16dp"
android:text="userName"
app:layout_constraintBottom_toTopOf="#+id/passwordView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="#+id/passwordView"
android:layout_width="300dp"
android:layout_height="25dp"
android:layout_marginBottom="16dp"
android:text="password"
app:layout_constraintBottom_toTopOf="#+id/noteView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="#+id/noteView"
android:layout_width="300dp"
android:layout_height="30dp"
android:layout_marginBottom="16dp"
android:text="note"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/delButton"
android:layout_width="78dp"
android:layout_height="40dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="36dp"
android:background="#E6360F"
android:text="#string/delete_credential_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
MainActivity.kt
package com.example.passwordmanager
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL,false)
val db = DataBaseHandler(this)
val detailsData = db.readCredentials()
val adapter = CredentialAdapter(detailsData,this,{credentialsModel: CredentialsModel->deleteClick(credentialsModel)})
recyclerView.adapter = adapter
}
fun deleteClick(credential: CredentialsModel){
val db = DataBaseHandler(this)
if(db.deleteData(credential.id)){
//adapter.notifyItemRemoved(position)
Toast.makeText(applicationContext,"Deleted", Toast.LENGTH_SHORT).show()
}
}
fun addNewCredentials(view : View){
print("hello world")
val intent = Intent(this, AddDetailActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
}
CredentialAdapter.kt
package com.example.passwordmanager
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.list_item_layout.view.*
class CredentialAdapter(
private val items: List<CredentialsModel>,
ctx: Context, val clickListener: (CredentialsModel) -> Unit
): RecyclerView.Adapter<CredentialAdapter.ViewHolder>() {
var context = ctx
class ViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
fun bind(credential: CredentialsModel,clickListener: (CredentialsModel) -> Unit){
itemView.urlView.text = credential.url
itemView.userNameView.text = credential.userName
itemView.passwordView.text = credential.password
itemView.noteView.text = credential.note
itemView.delButton.setOnClickListener{clickListener(credential)}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item_layout,parent,false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val credential:CredentialsModel = items[position]
holder.bind(credential,clickListener)
}
}
add remove setOnClickListener in your onBindViewHolder.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.remove.setOnClickListener {
val db = DataBaseHandler(this)
if(db.deleteData(credential.id)){
notifyItemRemoved(holder.getAdapterPosition())
}
}
}
The best way to handle these kinds of situations is to use LiveData.
LiveData is basically an observable class which reads data only when there is a change.
What you can do is create a set function in your adapter like:
internal fun setData(data: List<Data>) {
this.data= dataList //this datalist is a list defined in your adapter
notifyDataSetChanged()
}
now in your main activity/fragment, create a LiveData List outside the onCreate function like this:
private lateinit var allData:LiveData<List<Data>>
Now inside your onCreate function, use can observe the livedata and set the data for recyclerview like this:
allData.observe(this, Observer { data->
data?.let { adapter.setData(it) }
})
You are deleting the item from database but not from the list inside recyclerview adapter.
class CredentialAdapter(
private val items: ArrayList<CredentialsModel>, // Change list to arraylist
ctx: Context, val clickListener: (CredentialsModel, Int) -> Unit
): RecyclerView.Adapter<CredentialAdapter.ViewHolder>() {
...
...
fun remove(position: Int) {
// Remove and notify the adapter to reload
items.removeAt(position)
notifyItemRemoved(position)
}
class ViewHolder(itemView: View):RecyclerView.ViewHolder(itemView) {
fun bind(credential: CredentialsModel,clickListener: (CredentialsModel, Int) -> Unit){
...
...
// Pass adapter item position so that we can update the list after delete
itemView.delButton.setOnClickListener{clickListener(credential, adapterPosition)
}
}
...
...
}
Inside MainActivity.kt
fun deleteClick(credential: CredentialsModel, position: Int) {
val db = DataBaseHandler(this)
if(db.deleteData(credential.id)){
adapter.remove(position)
Toast.makeText(applicationContext,"Deleted", Toast.LENGTH_SHORT).show()
}
}
use ListAdpater
class AdapterMain(var onClickListener: (Int) -> Unit) :
ListAdapter<Note, AdapterMain.NoteViewHolder>(DIFFCALBACK) {
companion object DIFFCALBACK : DiffUtil.ItemCallback<Note>() {
override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.title == newItem.title &&
oldItem.description == newItem.description &&
oldItem.priority == newItem.priority
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.note_item, parent, false)
return NoteViewHolder(view)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.txtTitle.text = getItem(position).title
holder.txtDesc.text = getItem(position).description
holder.txtPriority.text = getItem(position).priority.toString()
}
inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var txtTitle: TextView = itemView.txt_title
var txtDesc: TextView = itemView.txt_desc
var txtPriority: TextView = itemView.txt_priority
init {
itemView.setOnClickListener { onClickListener(adapterPosition) }
}
}
fun getNoteAt(position: Int): Note {
return getItem(position)
}
}
you can see complete code of simple NoteApp with kotlin , recyclerView , MVVM and..
class coba : AppCompatActivity() {
private lateinit var recycleView :RecyclerView
private lateinit var datalis :ArrayList
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coba)
recycleView = findViewById(R.id.rcycoba)
datalis = ArrayList()
val dtnama = arrayOf(
"Danial Sanganus",
"Bijonia Skolin",
"Alianes Pertoli",
"Sivanian Pertici",
"Olehsan alausi"
)
for (i in dtnama.indices){
datalis.add(
dataCoba(
dtnama[i]
)
)
populateData()
}
}
private fun populateData(){
val linearManager = LinearLayoutManager(this)
linearManager.reverseLayout=true
linearManager.stackFromEnd=true
recycleView.layoutManager=linearManager
val adp =adpCoba(this,datalis)
recycleView.adapter=adp
}
}
I have a list of 5 elements in a recyclerview, set up like a to do list. There is a listener on the checkbox in each row, and for the purposes of this minimal reproducible example whenever you check any boxes it randomly sets the value of the 5 checkboxes. When an item is unchecked, it should appear in black text, and when an item is checked it should appear in gray text and italic.
When I check a box and reset the values, usually the UI updates as expected. However, sometimes one item sticks in the wrong layout so the checkbox shows the correct value but the text style is wrong. Why is this behavior inconsistent and how can I ensure the UI is refreshed every time?
Here's the entire MRE:
MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ActivityMainBinding
import kotlin.random.Random
class MainActivity : AppCompatActivity() {
private lateinit var adapter: ToDoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.itemsList.layoutManager = LinearLayoutManager(this)
val onCheckboxClickListener: (ToDoItem) -> Unit = { _ ->
adapter.submitList(getSampleList())
}
adapter = ToDoAdapter(onCheckboxClickListener)
binding.itemsList.adapter = adapter
adapter.submitList(getSampleList())
}
private fun getSampleList(): List<ToDoItem> {
val sampleList = mutableListOf<ToDoItem>()
sampleList.add(ToDoItem(id=1, description = "first item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=2, description = "second item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=3, description = "third item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=4, description = "fourth item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=5, description = "fifth item", completed = Random.nextBoolean()))
return sampleList
}
}
ToDoItem.kt
data class ToDoItem(
var id: Long? = null,
var description: String,
var completed: Boolean = false
)
ToDoAdapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ChecklistItemCheckedBinding
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ChecklistItemUncheckedBinding
const val ITEM_UNCHECKED = 0
const val ITEM_CHECKED = 1
class ToDoAdapter(private val onCheckboxClick: (ToDoItem) -> Unit): ListAdapter<ToDoItem, RecyclerView.ViewHolder>(ToDoItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_CHECKED -> ViewHolderChecked.from(parent)
else -> ViewHolderUnchecked.from(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val toDoItem = getItem(position)
when (holder) {
is ViewHolderChecked -> {
holder.bind(toDoItem, onCheckboxClick)
}
is ViewHolderUnchecked -> {
holder.bind(toDoItem, onCheckboxClick)
}
}
}
override fun getItemViewType(position: Int): Int {
val toDoItem = getItem(position)
return when (toDoItem.completed) {
true -> ITEM_CHECKED
else -> ITEM_UNCHECKED
}
}
class ViewHolderChecked private constructor(private val binding: ChecklistItemCheckedBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem) -> Unit) {
binding.todoItem = toDoItem
binding.checkboxCompleted.setOnClickListener {
onCheckboxClick(toDoItem)
}
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolderChecked {
val layoutInflater = LayoutInflater.from(parent.context)
return ViewHolderChecked(ChecklistItemCheckedBinding.inflate(layoutInflater, parent, false))
}
}
}
class ViewHolderUnchecked private constructor(private val binding: ChecklistItemUncheckedBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem) -> Unit) {
binding.todoItem = toDoItem
binding.checkboxCompleted.setOnClickListener {
onCheckboxClick(toDoItem)
}
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolderUnchecked {
val layoutInflater = LayoutInflater.from(parent.context)
return ViewHolderUnchecked(ChecklistItemUncheckedBinding.inflate(layoutInflater, parent, false))
}
}
}
}
class ToDoItemDiffCallback : DiffUtil.ItemCallback<ToDoItem>() {
override fun areItemsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return oldItem == newItem
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<RelativeLayout
android:id="#+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/items_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scrollbars="none" />
</RelativeLayout>
</layout>
checklist_item_checked.xml
<?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="todoItem"
type="com.dalydays.android.mre_recyclerview_refresh_last_item.ToDoItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="#+id/checkbox_completed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:checked="#{todoItem.completed}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tv_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:text="#{todoItem.description}"
android:textAppearance="?attr/textAppearanceListItem"
android:textColor="#65000000"
android:textStyle="italic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/checkbox_completed"
app:layout_constraintTop_toTopOf="parent"
tools:text="Mow the lawn" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
checklist_item_unchecked.xml
<?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="todoItem"
type="com.dalydays.android.mre_recyclerview_refresh_last_item.ToDoItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="#+id/checkbox_completed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:checked="#{todoItem.completed}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tv_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:text="#{todoItem.description}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/checkbox_completed"
app:layout_constraintTop_toTopOf="parent"
tools:text="Mow the lawn" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Modify these methods
override fun areItemsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return ((oldItem.id == newItem.id) && (oldItem.description == newItem.description) && (oldItem.completed == newItem.completed)
}
Also override the methods getNewListSize() & getOldListSize()
Maybe not the "best" solution, but this is what I came up with. I determined that there is probably an issue with the timing between the checkbox animation and the recyclerview refresh animation. Sometimes, depending on how long it takes the recyclerview to diff, the recyclerview can attempt to refresh before or after the checkbox finishes animating. When it finishes before, the checkbox animation blocks the recyclerview animation and leaves the UI in the wrong state. Otherwise it appears to work as intended.
I decided to manually run adapter.notifyItemChanged(position) even though RecyclerView.ListAdapter is supposed to handle this automatically. It still animates somewhat inconsistently depending on when the diff finishes calculating, but it's much better than leaving the UI in a bad state, and it's much better than refreshing the entire list every time with notifyDataSetChanged().
In MainActivity, change the checkboxlistener to this:
val onCheckboxClickListener: (ToDoItem, Int) -> Unit = { _, position ->
adapter.submitList(getSampleList())
adapter.notifyItemChanged(position)
}
In ToDoAdapter, change class header to this:
class ToDoAdapter(private val onCheckboxClick: (ToDoItem, Int) -> Unit): ListAdapter<ToDoItem, RecyclerView.ViewHolder>(ToDoItemDiffCallback()) {
And change both bind() functions to this:
fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem, Int) -> Unit) {
binding.todoItem = toDoItem
binding.checkboxCompleted.setOnClickListener {
onCheckboxClick(toDoItem, layoutPosition)
}
binding.executePendingBindings()
}