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}")
}
}
Related
I'm using Kotlin to create an event that generates a toast message on click of a recyclerview. I run into trouble making a Tost message in a recyclerview event.
I tried the following page, but couldn't solve it.
Toast message is not working in Recycler View
error code is
in kotlin & None of the following functions can be called with the arguments supplied: public open fun makeText(p0: Context!, p1: CharSequence!, p2: Int): Toast! defined in android.widget.Toast public open fun makeText(p0: Context!, p1: Int, p2: Int): Toast! defined in android.widget.Toast
PrintActivity.kt
package com.questionbank
class PrintActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vBinding = ActivityPrintBinding.inflate(layoutInflater)
setContentView(vBinding.root)
val helper = SqliteHelper(this, "myDB.sql", 1)
var recyclerViewAdapter = CustomAdapter()
recyclerViewAdapter.listData = helper.select()
vBinding.myRecyclerView.adapter = recyclerViewAdapter
vBinding.myRecyclerView.layoutManager = LinearLayoutManager(this)
vBinding.myRecyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
}
class CustomAdapter : RecyclerView.Adapter<CustomAdapter.Holder>() {
var listData = ArrayList<questionType>()
inner class Holder(val vBinding: QuestionLayoutRecyclerBinding) :
RecyclerView.ViewHolder(vBinding.root) {
fun setData(id:Int?, question: String, answer: String, exp: String) {
vBinding.printId.text=id.toString()
vBinding.myLinear.setOnClickListener {
// error occur
Toast.makeText(this#PrintActivity, "test", Toast.LENGTH_SHORT).show()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val vBinding = QuestionLayoutRecyclerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return Holder(vBinding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val question = listData[position]
holder.setData(question.id, question.question, question.answer, question.exp)
}
override fun getItemCount(): Int {
return listData.size
}
}
}
activity_print
<?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=".PrintActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/myRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
question_layout_recycler.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/myLinear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="#+id/printId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView" />
</LinearLayout>
Two ways to fix,
Make CustomAdapter class as inner class.
innner class CustomAdapter : RecyclerView.Adapter<CustomAdapter.Holder>() {
So toast function it will take constant from activity class.
In viewholder, get context from view. it.context will get context from linearlayout.
vBinding.myLinear.setOnClickListener {
Toast.makeText(it.context, "test", Toast.LENGTH_SHORT).show()
}
Its recommended to place adapter logic in separate file and use second solution.
So you dont need to make adapter as inner class.
I am working with nested recyclerView. According to business logic, first I have to call an API that fetched the list of item that is shown in the Parent recycler view. After that, if a user clicks any of the items of the parent recycler view, another API is called which fetches a list of sub-items, and I have to show the item list in the inner recycler view of that clicked-positioned parent recycler view.
I successfully implemented showing items in the parent recycler view and sub-items in the clicked-position nested recycler view.
But the problem I am facing is when I clicked any specific item of the parent recycler view, all the other nested recycler view's item get changed with the newly populated sub-items value.
How can I solve this issue?. Here is the sample code
main_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
style="#style/CardViewStyle"
android:id="#+id/chapter_layout"
android:layout_width="match_parent">
<com.google.android.material.textview.MaterialTextView
android:id="#+id/chapter_name_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.google.android.material.textview.MaterialTextView
android:id="#+id/header_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/chapter_topic_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="#layout/item_main" />
</androidx.appcompat.widget.LinearLayoutCompat>
MainItemViewHolder.kt
class MainItemViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: SpecificChapter, onClick: (SpecificChapter, Int) -> Unit) {
with(view) {
chapter_name_text_view.text = "Chapter "+ item.no
header_text_view.text = item.name
chapter_layout.setOnClickListener { onClick(item, bindingAdapterPosition) }
}
}
}
MainItemAdapter.kt
class MainItemAdapter(
val onClick: (SpecificChapter, Int) -> Unit
) : ListAdapter<SpecificChapter, MainItemViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<SpecificChapter>() {
override fun areItemsTheSame(old: SpecificChapter, aNew: SpecificChapter) = (old.id == aNew.id)
override fun areContentsTheSame(old: SpecificChapter, aNew: SpecificChapter) = (old == aNew)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainItemViewHolder {
return MainItemViewHolder(parent.inflate(R.layout.main_item))
}
override fun onBindViewHolder(holder: MainItemViewHolder, position: Int) {
holder.bind(getItem(position)!!, onClick)
}
}
sub_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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">
<com.google.android.material.textview.MaterialTextView
android:id="#+id/chapter_topic_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</com.google.android.material.card.MaterialCardView>
SubItemViewHolder.kt
class SubItemViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: SpecificTopic, onClick: (SpecificTopic) -> Unit) {
with(view) {
chapter_topic_text_view.text = item.name
chapter_topic_layout.setOnClickListener { onClick(item) }
}
}
}
SubItemAdapter.kt
class SubItemAdapter(
val onClick: (SpecificTopic) -> Unit
) : ListAdapter<SpecificTopic, SubItemViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<SpecificTopic>() {
override fun areItemsTheSame(old: SpecificTopic, aNew: SpecificTopic) = (old.id == aNew.id)
override fun areContentsTheSame(old: SpecificTopic, aNew: SpecificTopic) = (old == aNew)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubItemViewHolder {
return SubItemViewHolder(parent.inflate(R.layout.sub_item))
}
override fun onBindViewHolder(holder: SpecificTopicViewHolder, position: Int) {
holder.bind(getItem(position)!!, onClick)
}
}
activity_chapter.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/chapter_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/main_item"/>
</androidx.appcompat.widget.LinearLayoutCompat>
ChapterActivity.kt
class ChapterActivity : BaseActivity<ChapterViewModel>() {
var chapterPosition = 0
private val mainItemAdapter: MainItemAdapter by lazy {
MainItemAdapter { specificChapter, position ->
// sub item Api call when clicked specific item. response is viewmodel.allTopics
chapterPosition = position
specificChapter.id?.let { viewModel.getChapterWiseTopics(it) }
}
}
private val subItemAdapter: SubItemAdapter by lazy {
SubItemAdapter {
}
}
override fun onResume() {
super.onResume()
// Main item Api call. response is viewmodel.allChapters
viewModel.getChapters("subject_code")
}
override fun observeLiveData() {
// showing Main item in parent recyclerView
observe(viewModel.allChapters) {
val chapterList = ArrayList<SpecificChapter>()
chapterList.add(SpecificChapter(...))
chapter_recycler_view.adapter = mainItemAdapter
mainItemAdapter.submitList(chapterList)
chapter_recycler_view.setItemViewCacheSize(chapterList.size)
}
// showing sub item in nested recyclerView
observe(viewModel.allTopics) {
val topicList = ArrayList<SpecificTopic>()
topicList.add(SpecificTopic(...))
with((chapter_recycler_view.findViewHolderForAdapterPosition(chapterPosition) as MainItemViewHolder).itemView) {
this.chapter_topic_recycler_view.adapter = subItemAdapter
subItemAdapter.submitList(topicList)
}
}
}
}
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."
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()
}