I am using FirestorePagingAdapter to load data from firestore and display them in recyclerView.
I get the item and bind it to the view.
override fun onBindViewHolder(holder: MyViewHolder, position: Int, model: Question) {
holder.bind(model)
}
fun bind(question: Question) {
//here I load questionData in my viewholder
}
I set onClickListener to textViews in my viewHolder, my question model has up to 5 options which are shown in my viewHolder, so when user presses one I want to update data to the model
override fun onClick(v: View) {
val pressedQuestion = getItem(adapterPosition)?.toObject(Question::class.java)
}
Inside clickListener I get pressed question by calling getItem and toObject.
There I want to update field pressedQuestion.questionId = "answered"
But when this view is scrolled of the screen and than back on sreen data doesn't seem to be changed, questionId field remains the same even though I set it to "answered".
I tried calling every version of notifyDatasetChanged method but it doesn't work.
How can I change data that FirestorePagingAdapter is loading in my viewHolder from inside onClickListener
Related
I just saw this example class for an Adapter for Recyclerview and I'm a bit confused on how it knows to call onCreateViewHolder, onBindViewHolder, etc just from adding an Item object to a list?
Does it have something to do with the line notifyItemInserted(items.size - 1) ?
Is it that whenever this method is called the onCreateViewHolder method is recalled with for that item or?
Adapter:
class ListAdapter (
private val items: MutableList<Item>
) : RecyclerView.Adapter <ListAdapter.ListViewHolder>() {
class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.list_items, parent, false)
)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val currItem = items[position]
holder.itemView.apply {
tv_item.text = currItem.title
cb_item.isChecked = currItem.checked
crossItem(tv_item, currItem.checked)
cb_item.setOnCheckedChangeListener { _, isChecked ->
crossItem(tv_item, isChecked)
currItem.checked = !currItem.checked
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
}
}
}
override fun getItemCount(): Int {
return items.size
}
private fun crossItem (itemText: TextView, checked: Boolean) {
if (checked){
//dk wtf paint flags is
itemText.paintFlags = itemText.paintFlags or STRIKE_THRU_TEXT_FLAG
}
//else remove
else {
itemText.paintFlags = itemText.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
fun addItem (item: Item){
items.add (item)
notifyItemInserted(items.size - 1)
}
}
Item Class:
data class Item (
val title: String,
var checked: Boolean = false
)
{
}
Whenever the Adapter needs to provide a new view for the RecyclerView to draw, it checks if it has an unused ViewHolder in its pool. If it doesn't, it calls onCreateViewHolder() so it can create one. Then it calls onBindViewHolder() for the ViewHolder that came from either source so the contained view can be prepared before being added to the layout.
If you call one of the notify methods, that triggers it to refresh whichever item rows are affected. It will return any removed rows to the ViewHolder pool and then follow the above steps to get the views it needs for new rows. If you use a notify...changed method, it will only need to use onBindViewHolder() for the applicable rows. When you use the nuclear option notifyDataSetChanged(), it returns all items to the pool.
When the RecyclerView is first displayed, or when the layout is resized, those actions will possibly trigger the need to show more rows. When you scroll the list, items that scroll off the screen are returned to the ViewHolder pool, and when new items scroll into view, ViewHolders need to be created or acquired from the pool as explained above.
By the way, this is going to look ugly because it refreshes the whole list even though only some items are removed:
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
I recommend this instead so you get a nice transition:
for (i in items.indices.reversed()) {
if (items[i].checked) {
items.removeAt(i)
notifyItemRemoved(i)
}
}
I iterate in reverse so the indices that are removed are stable as you iterate and remove items.
override fun getItemCount(): Int {
return items.size
}
This function is the key, it knows how many to create and how many to bind by knowing how many there are in total. The amount of ViewHolders created is more based on how many Views can fit on the screen at one time.
This gets more complex when you have different view types, as it will sometimes has to create more ViewHolders than what was required from the start as view types change.
The notify... functions just let the Adapter know it needs to "re-look" at the List.
I have a RecyclerView, and I'm trying to get it to scroll down every time a new message pops up. I struggle to do so. I'm not even sure If I am doing it in the right place. I'm doing it inside of my RecyclerView adapter inside of the function onBindViewHolder(). Here is the code:
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
holder.itemView.apply {
tvTitle.text = messages[position].title //here I am setting the title - the name of the person who wrote the message
tvMessage.text = messages[position].message //here I am setting the message - the message written by the person
rvTodos.smoothScrollToPosition(itemCount - 1) //and here, at last, I am setting the my recyclerview(rvTodos) to smoothly scrool down to the itemCount position (the size of the messages array)
}
}
override fun getItemCount(): Int {
return messages.size
}
I was simply not updating the recyclerview in the correct place. I was attempting to update it inside of the recyclerview adapter when it actually needs to be updated inside of my activity or viewmodel - where I update the contents of my messagesArrayList! Solved by: Tendai (in the comments of the question)
I've got an RecyclerView that lists articles, in each article/item in the onBindViewHolder I'm loading another RecyclerView for the comments and I need to update them when the user returns to the activity.
Is there a way to detect the activity OnResume inside onBindViewHolder of the parent RecyclerView?
I assume that you have some data class for article which holds its details and comments. You pass this list to first recycler which in onBind is filling comment recyclers with data. If you have to update comments just pass list of articles with updated comments to article's adapter. It will trigger onBindViewHolder inside which you will have new comments. In code it would be somethings like this:
data class Article (
val name: String
... other details
val comments: List<Comment>
)
onBindViewHolder for Article's adapter:
override fun onBindViewHolder(holder: Holder, position: Int) {
val article = getItem(position)
// here you should submit list of comments
commentsAdapter.submitList(article.comments)
}
and in Activity:
override fun onResume() {
super.onResume()
//get new comments here and set it for new articles
val newArticles = oldArticles.map { article -> it.copy(comments = newComments)}
articlesAdapter.submitList(newArticles)
}
By the way it seems like AdapterDelegates would be even better solution to your problem. Read more here: http://hannesdorfmann.com/android/adapter-delegates
I have a custom view in which you can draw with your fingers, I fill out a recyclerview with this view, made an adapter, and for example a list with size = 10 is obtained, where each item is a custom view, and if the user draws for example in item with position 5 then when scrolling the same picture appears in another item where he did not draw.
here is my adapter:
class CustomViewListAdapter : RecyclerView.Adapter<CustomViewListAdapter.ViewHolder>() {
private var listSchedule = ArrayList<Int>()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var calendarView: CustomView= itemView.calendar!!
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.schedule_item,
parent,
false
)
)
}
override fun getItemCount(): Int = listSchedule.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.calendarView.setValue(listSchedule[position] )
}
fun setSchedules(listSchedule: ArrayList<Int>) {
this.listSchedule = listSchedule
notifyDataSetChanged()
}
}
Are you aware of how RecyclerView works? Lets assume you have 1000 items in your list. A simple approach would be to create 1000 views from your schedule_item layout bind them and show them in e.g. a scrollview. However, doing that would cost a lot of time and memory. So the clever Android developers came up with the following idea:
Since of the 1000 items only e.g. 10 are visible at the same time, lets just create 10 views and only change the content of the views depending on the actual item they show. So the 10 views are reused or recycled.
For this to work, the onBindViewHolder implementation must make sure that it updates the provided viewholder in a way that it shows the content of the item at the given position.
Now in your code, all you do in onBindViewHolder is, to set a single integer. There is no sign of setting any custom drawing etc. So I assume the drawing is just stored inside the CustomView. And since there are only those 10 or so CustomViews (as explained above), when they are reused to show a different item, they contain the original drawings because you didn't change that in onBindViewHolder.
I'm trying to adopt PagedList to my app. One of the functionality I want is the ability to handle button click event within list item.
I was thinking to utilize ViewModel to listen for the click event and taken from the example https://medium.com/#star_zero/singleliveevent-livedata-with-multi-observers-384e17c60a16, I was successfully getting the click event.
so I have,
LivePagedListBuilder(DeviceDataSourceFactory(), defaultConfig)
.build()
.observe(this, Observer { list ->
// here is where I have Observer for the click event
// for example, list?.forEach { it.event.observe(...) }
// but this block isn't called everytime
adapter.submitList(list)
})
As the comment above, I don't always get notified when new item is added to the list. I guess I only got it once from loadInitial and once from loadAfter. After ref is linked, PagedList handles updating the list itself without notifying the Observer. Therefor, I can't correctly setup click event Observer. Any help would be appreciated. been blocked for more than a week. Thanks!
You can always pass the reference to your OnClickListener to your Adapter class.
For example:
//Reference of click listener in your Adapter class
//Initialise clickListener in adapter's init method, You can pass ViewModel's reference to the adapter as the reference of YOUR_LISTENER_IMPL
val clickListener : YOUR_LISTENER_IMPL
//ViewHolder class
class YOUR_VIEW_HOLDER(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(data: YOUR_DATA_TYPE, clickListener: (YOUR_DATA_TYPE) -> Unit) {
itemView.setOnClickListener { clickListener(data)}
}
}
//onBindViewHolder method inside Adapter
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// Populate ViewHolder with data that corresponds to the position in the list
// which we are told to load
(holder as YOUR_VIEW_HOLDER).bind(DATA_SOURCE_LIST[position], clickListener)
}
Then while initialising your adapter, you can send the clickListener object via constructor. This way all your ViewHolder instances will always have a clickListener and you won't be missing any click events, when the items gets added into the list.
I hope this helps.