RecyclerView AsyncListDiffer and data source consistency state lose with onClickListener - android-recyclerview

I have data source(in that example it's just a var myState: List)
class MainActivity : AppCompatActivity() {
var generation: Int = 0
var myState: List<User> = emptyList()
val userAdapter = UserAdapter {
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recycler = findViewById<RecyclerView>(R.id.rvContent)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = userAdapter
Thread {
while (true) {
generateNewData()
Handler(mainLooper).post {
userAdapter.submit(myState)
}
sleep(3000L)
}
}.start()
}
fun generateNewData() {
generation++
myState = (0..5000).map { User("$generation", it) }
}
}
I have RecyclerView, and AsyncListDiffer connected to it
data class User(val name: String, val id: Int) {
val createdTime = System.currentTimeMillis()
}
data class UserViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bindTo(user: User, action: (User) -> Unit) {
val textView = view.findViewById<TextView>(R.id.title)
textView.text = "${user.name} ${user.id} ${user.createdTime}"
textView.setOnClickListener { action(user) }
}
}
class UserAdapter(val action: (User) -> Unit) : RecyclerView.Adapter<UserViewHolder>() {
val differ: AsyncListDiffer<User> = AsyncListDiffer(this, DIFF_CALLBACK);
object DIFF_CALLBACK : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = differ.currentList[position]
holder.bindTo(user, action = action)
}
fun submit(list: List<User>) {
differ.submitList(list)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
I have OnClickListener binded to every item on RecyclerView
{
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")}
That listener checks if item that was clicked is exists in the data source, and if not, outputs it to the console.
Every few seconds data in the source are changed, and pushed to
a AsyncListDiffer via submitList method, some how internally it uses other thread to match data and pass that diffed data
to the RecyclerView, and that takes some time;
If I starts clicking on the items non-stop, and the click event occurs at the same time when the differ inserts new data, then I get into a non-consistent state.
So, how to handle that?
Ignore a click with inconsistent data?(cons: User can see some strange behaviour like list item not collapse/expand, no navigation happen, etc)
Try to find a similar item in the new data by separate fields(positions/etc), and use it?(cons: same as 1. but less probability)
Block OnClickListener events until the data is consistent in both the Recycler and the data source? (cons: same as above, and also lag with action user performed until data became consistent again)
Something else? What is a best way to solve that?

Related

RecycleView selection library - holding an item fires onSelectionChanged several times

I implemented a simple RecyclerView example, with multiple items selection possibility, using the selection library.
Here's my code:
class MainActivity : AppCompatActivity() {
private var listOfItems = ArrayList<Item>()
private var selectionTracker: SelectionTracker<Long>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.rvMainRecyclerView)
for (i in 0..32) {
listOfItems.add(Item(i, "Item $i"))
}
val recyclerViewAdapter = RecyclerViewAdapter(listOfItems)
recyclerView.adapter = recyclerViewAdapter
selectionTracker = SelectionTracker.Builder<Long>(
"Itemselection",
recyclerView,
MyItemKeyProvider(recyclerView),
MyItemDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
).build()
selectionTracker?.addObserver(
object: SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
val itemsSelected = selectionTracker?.selection?.size()
Log.d("MainActivity", "$itemsSelected items selected")
}
}
)
recyclerViewAdapter.tracker = selectionTracker
}
}
class RecyclerViewAdapter(
val dataSet: ArrayList\<Item\>
): RecyclerView.Adapter\<RecyclerViewAdapter.ViewHolder\>() {
var tracker: SelectionTracker<Long>? = null
init {
setHasStableIds(true)
}
inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val textView: TextView
init {
textView = view.findViewById(R.id.tvRecyclerViewItemTitle)
}
fun getItemDetails(): ItemDetails<Long> {
return object : ItemDetails<Long>() {
override fun getPosition(): Int {
return adapterPosition
}
override fun getSelectionKey(): Long? {
return itemId
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater
.from(parent.context).inflate(R.layout.recycler_view_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = dataSet[position]
holder.textView.text = currentItem.value
holder.itemView.isActivated = tracker!!.isSelected(position.toLong())
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.toLong()
}
}
I'm curious why the onSelectionChanged (in MainActivity selectionTracker?.addObserver...) is triggered several times while I hold an item to start selection mode?
To be more precise, this is the use case:
Nothing is selected,
Long click on any item => selection mode activated; the item is selected
Here, "1 items selected" is being printed out as long as I hold the first item.

Kotlin RecyclerView not updating after data changes

I am using RecyclerView to display a dynamic list of data and after I call an api I need to update my RecyclerView UI but the items in my RecyclerView does not change...
Below is my how I init my RecyclerView in my Fragment:-
forwardedList.layoutManager = LinearLayoutManager(context!!, RecyclerView.VERTICAL, false)
adapter = ForwardListAdapter(SmsHelper.getForwardedSms(context!!))
forwardedList.adapter = adapter
Below is my custom RecyclerView Adapter:-
class ForwardListAdapter(val forwardedList: List<SmsData>) : RecyclerView.Adapter<ForwardListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ForwardListAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.forwarded_item, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ForwardListAdapter.ViewHolder, position: Int) {
holder.bindItems(forwardedList[position])
}
override fun getItemCount(): Int {
return forwardedList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(sms: SmsData) {
val simSlotText: TextView = itemView.findViewById(R.id.simSlot)
val senderText: TextView = itemView.findViewById(R.id.sender)
simSlotText.text = "[SIM ${sms.simSlot}] "
senderText.text = sms.sender
}
}
}
I am currently updating my RecyclerView from SmsHelper class as below:-
val fragments = mainActivity!!.supportFragmentManager.fragments
for (f in fragments) {
if (f.isVisible) {
if (f.javaClass.simpleName.equals("ForwardedFragment")) {
val fg = f as ForwardedFragment
fg.adapter.notifyDataSetChanged() <----- HERE
} else if (f.javaClass.simpleName.equals("FailedFragment")) {
val fg = f as FailedFragment
fg.adapter.notifyDataSetChanged()
}
}
}
As I observed, you did not really change the adapter's data but only called notifyDataSetChanged. You cannot just expect the data to be changed automatically like that since notifyDataSetChanged only:
Notifies the attached observers that the underlying data has been changed and any View reflecting the data set should refresh itself.
You need to change the data by yourself first, then call notifyDataSetChanged.
class ForwardListAdapter(private val forwardedList: MutableList<SmsData>) : RecyclerView.Adapter<ForwardListAdapter.ViewHolder>() {
// ...
fun setData(data: List<SmsData>) {
forwardedList.run {
clear()
addAll(data)
}
}
// ...
}
Then do it like this:
adapter.run {
setData(...) // Set the new data
notifyDataSetChanged(); // notify changed
}

How to search a diffutil filter out results from existing list

I'm using DiffUtil in my RecyclerView to displays a list from a database using the Room component. I would like to add a search function in the Appbar, that will filter out the existing items in the list as the user is typing.
My app currently has a search icon in the action bar, when you click the search icon it will expand across the Appbar and allow the user to search the database and return a new list. This mehtod involves querying the database each time.
Search Menu, This is where the parameters for the search widget are set.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="#+id/my_search"
android:title="Search"
android:icon="#drawable/ic_search"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>
RecyclerViewFragment
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.recycler_view_menu, menu)
val searchItem = menu.findItem(R.id.my_search)
val searchView: SearchView = searchItem.actionView as SearchView
searchView.imeOptions = EditorInfo.IME_ACTION_DONE
searchView.setIconifiedByDefault(false)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
// This approach queries my database for a new list.
viewModel.searchTopic("%$query%")
submitList()
return false
}
override fun onQueryTextChange(newText: String): Boolean {
// I would like to use the onTextChange() to filter out results from the list instead of querying a new list from the database.
return true
}
})
}
private fun submitList() {
viewModel.listDevTopics.observe(viewLifecycleOwner, Observer {
it?.let {
rvAdapter.submitList(it)
}
})
}
My RecyclerViewAdapter
class RecyclerViewAdapter() : androidx.recyclerview.widget.ListAdapter<Dev,
RecyclerViewAdapter.ItemViewHolder>(MyDiffCallback()) {
lateinit var searchList: List<Dev>
class MyDiffCallback : DiffUtil.ItemCallback<Dev>() {
override fun areItemsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem.topic == newItem.topic
}
override fun areContentsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem == newItem
}
}
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
...
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
...
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
...
}
I would like to avoid querying the database every time for a search result, I want to use onQueryTextChange so it goes through the existing list and updates the list as the user is entering their query.
just implement Filterable and override getFilter Method
and make your filter object then return this object at getFilter Method
class JobOrderAdapter(val clickListener: JobOrderListener) : ListAdapter<CJO,
ViewHolder>(JobOrderDiffCallback()), Filterable {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder.from(parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
}
private var list = listOf<CJO>()
fun setData(list: List<CJO>){
this.list = list
submitList(list)
}
override fun getFilter(): Filter = customFilter
private val customFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList = mutableListOf<CJO>()
if (constraint == null || constraint.isEmpty()) {
filteredList.addAll(list)
} else {
val filterPattern = constraint.toString().toLowerCase().trim()
for (item in list) {
// here i am searching at custom obj by managerName
if (item.managerName.toLowerCase().contains(filterPattern)) {
filteredList.add(item)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}
override fun publishResults(constraint: CharSequence?, filterResults: FilterResults?) {
submitList(filterResults?.values as MutableList<CJO>?)
}
}}
and from your fragmnet or activity just call adapter.filter.filter(yourQueryText)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.home_menu, menu)
val searchByContract = menu.findItem(R.id.search_by_name)
val searchContractView = searchByContract.actionView as SearchView
searchContractView.queryHint = "البحث باسم مدير البيع"
searchContractView.inputType = InputType.TYPE_CLASS_TEXT
searchContractView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
adapter.filter.filter(newText)
return false
}
})
super.onCreateOptionsMenu(menu, inflater)
}

How can I convert this Firebase Recycler Adapter implementation into an Adapter.kt class?

I have written a Kotlin FirebaseRecyclerAdapter that works just fine as part of my MainActivity. However, I would like to have this code in a separate MainAdapter.kt file/class. How can I do this?
var query = FirebaseDatabase.getInstance()
.reference
.child("").child("categories")
.limitToLast(50)
val options = FirebaseRecyclerOptions.Builder<Category>()
.setQuery(query, Category::class.java)
.setLifecycleOwner(this)
.build()
val adapter = object : FirebaseRecyclerAdapter<Category, CategoryHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryHolder {
return CategoryHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.category_row, parent, false))
}
protected override fun onBindViewHolder(holder: CategoryHolder, position: Int, model: Category) {
holder.bind(model)
}
override fun onDataChanged() {
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
}
}
class CategoryHolder(val customView: View, var category: Category? = null) : RecyclerView.ViewHolder(customView) {
fun bind(category: Category) {
with(category) {
customView.textView_name?.text = category.name
customView.textView_description?.text = category.description
}
}
}
Given your code you could do something like this :
class MainAdapter(lifecycleOwner: LifecycleOwner) : FirebaseRecyclerAdapter<Category, CategoryHolder>(buildOptions(lifecycleOwner)) {
companion object {
private fun buildQuery() = FirebaseDatabase.getInstance()
.reference
.child("").child("categories")
.limitToLast(50)
private fun buildOptions(lifecycleOwner:LifecycleOwner) = FirebaseRecyclerOptions.Builder<Category>()
.setQuery(buildQuery(), Category::class.java)
.setLifecycleOwner(lifecycleOwner)
.build()
}
class CategoryHolder(val customView: View, var category: Category? = null) : RecyclerView.ViewHolder(customView) {
fun bind(category: Category) {
with(category) {
customView.textView_name?.text = category.name
customView.textView_description?.text = category.description
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryHolder {
return CategoryHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.category_row, parent, false))
}
protected override fun onBindViewHolder(holder: CategoryHolder, position: Int, model: Category) {
holder.bind(model)
}
override fun onDataChanged() {
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
}
}
There are many other ways to handle this problem, this is just an encapsulated version of yours.

How to get the selected item from ListView in Kotlin?

Code Sample:
package tech.kapoor.listviewdemo
import android.content.Context
import android.graphics.Color
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import android.widget.TextView
import android.widget.AdapterView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val listView = findViewById<ListView>(R.id.main_listview)
var redColor = Color.parseColor("#FF0000")
listView.setBackgroundColor(redColor)
listView.adapter = CustomAdapter(this)
}
private class CustomAdapter(context: Context): BaseAdapter() {
private val mContext: Context
init {
mContext = context
}
override fun getCount(): Int {
return 80
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItem(position: Int): Any {
return position
}
override fun getView(position: Int, view: View?, viewGroup: ViewGroup?): View {
val textView = TextView(mContext)
textView.text = "Here comes the !!"
return textView
}
}
}
I'm trying to understand list view instead of recycler view to understand basics first.
Anybody knows how we get the selected row id/index value on selection or onclick and also how to perform some action on selection of a specific row in kotlin?
To populate listview you must have dataset. Dataset may be any list of either datatypes like Strings or you can use list of model class. Something like this:
This is my simple list of dataset which I will use in ListView:
val data = ArrayList<TopicDTO>()
data.add(TopicDTO("1", "Info 1", true))
data.add(TopicDTO("2", "Info 2", false))
data.add(TopicDTO("3", "Info 3", true))
data.add(TopicDTO("4", "Info 4", false))
I have created one model class named TopicDTO which contains id,title and its status.
Now let's populate this into ListView:
list.adapter = ButtonListAdapter(baseContext, data)
Here is a simple adapter:
class ButtonListAdapter(//Class for rendering each ListItem
private val context: Context, private val rowItems: List<TopicDTO>) : BaseAdapter() {
override fun getCount(): Int {
return rowItems.size
}
override fun getItem(position: Int): Any {
return rowItems[position]
}
override fun getItemId(position: Int): Long {
return rowItems.indexOf(getItem(position)).toLong()
}
private inner class ViewHolder {
internal var main_text: TextView? = null //Display Name
internal var subtitle: TextView? = null //Display Description
internal var can_view_you_online: Button? = null //Button to set and display status of CanViewYouOnline flag of the class
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var convertView = convertView
var holder: ViewHolder? = null
val mInflater = context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
holder = ViewHolder()
if (convertView == null) {
convertView = mInflater.inflate(R.layout.main_lp_view_item, null)
holder.main_text = convertView!!.findViewById(R.id.main_lp_text) as TextView
holder.subtitle = convertView.findViewById(R.id.main_lp_subtitle) as TextView
holder.can_view_you_online = convertView.findViewById(R.id.can_view_you_online) as Button
convertView.tag = holder
} else {
holder = convertView.tag as ViewHolder
}
val rowItem = rowItems[position]
val main_text: String
val subtitle: String
holder.main_text!!.text = rowItem.info
holder.subtitle!!.text = rowItem.info
if (rowItem.canViewYouOnline) {
holder.can_view_you_online!!.setBackgroundColor(context.resources.getColor(R.color.colorPrimary))
} else {
holder.can_view_you_online!!.setBackgroundColor(context.resources.getColor(R.color.colorAccent))
}
holder.can_view_you_online!!.setOnClickListener(object : View.OnClickListener {
internal var buttonClickFlag: Boolean = false
override fun onClick(v: View) { //The Onclick function allows one to click the button on the list item and set/reset the canViewYouOnline flag. It is working fine.
}
})
return convertView
}
}
Now you can get your selected item like this:
list.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
// This is your listview's selected item
val item = parent.getItemAtPosition(position) as TopicDTO
}
Hope you understands this.
You can use inside the getView() method something like:
view.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
//use getItem(position) to get the item
}
})
or using the lambda:
view.setOnClickListener({ v -> //use theItem(position) })
Just a tip:
I'm trying to understand list view instead of recycler view to understand basics first.
In my opinion in your projects you will use RecyclerView in 99% of the cases.
add OnItemClickListener in you oncreate()
listView.setOnItemClickListener{ parent, view, position, id ->
Toast.makeText(this, "You Clicked:"+" "+position,Toast.LENGTH_SHORT).show()
}
Add the array of Items in your CustomAdapter class.
class CustomAdptor(private val context: Activity): BaseAdapter() {
//Array of fruits names
var names = arrayOf("Apple", "Strawberry", "Pomegranates", "Oranges", "Watermelon", "Bananas", "Kiwi", "Tomato", "Grapes")
//Array of fruits desc
var desc = arrayOf("Malus Domestica", "Fragaria Ananassa ", "Punica Granatum", "Citrus Sinensis", "Citrullus Vulgaris", "Musa Acuminata", "Actinidia Deliciosa", "Solanum Lycopersicum", "Vitis vinifera")
//Array of fruits images
var image = intArrayOf(R.drawable.apple, R.drawable.strawberry, R.drawable.pomegranates, R.drawable.oranges, R.drawable.watermelon, R.drawable.banana, R.drawable.kiwi, R.drawable.tomato, R.drawable.grapes)
override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
val inflater = context.layoutInflater
val view1 = inflater.inflate(R.layout.row_data,null)
val fimage = view1.findViewById(R.id.fimageView)
var fName = view1.findViewById(R.id.fName)
var fDesc = view1.findViewById(R.id.fDesc)
fimage.setImageResource(image[p0])
fName.setText(names[p0])
fDesc.setText(desc[p0])
return view1
}
override fun getItem(p0: Int): Any {
return image[p0]
}
override fun getItemId(p0: Int): Long {
return p0.toLong()
}
override fun getCount(): Int {
return image.size
}
}
You can find the whole tutorial at: listview with onItemClickListener using kotlin