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)
Related
I have succeeded pulling JSON from Reddit API but I cannot put it into my RecyclerView. I set a Log to check if my JSON is empty or null, and the Log successfully print the desired output, so that means my JSON is not empty and contains the necessary data.
Here is my PostRowAdapter.kt
class PostRowAdapter(private val viewModel: MainViewModel)
: ListAdapter<RedditPost, PostRowAdapter.VH>(RedditDiff()) {
private var awwRow = listOf<RedditPost>()
override fun onBindViewHolder(holder: VH, position: Int) {
val binding = holder.binding
awwRow[position].let{
binding.title.text = it.title
}
}
override fun getItemCount() = awwRow.size
}
I thought my code was correct, but when I ran the app, the RecyclerView still blank. Where am I wrong?
your PostRowAdapter is populating awwRow list, which is empty on start and never updated, thus this RecyclerView will always contain 0 elements
if you are using ListAdapter and submitList(...) method then you shouldn't override getItemCount and you shouldn't have own data list, so remove these lines
private var awwRow = listOf<RedditPost>() // on top
override fun getItemCount() = awwRow.size // on bottom
if you want access to whole list set with submitList() method then you can call inside adapter getCurrentList(). if you need a particular item at position (like in onBindViewHolder) then use getItem(position). So instead of
awwRow[position].let{
you should have
getItem(position).let{
I have a list that gets updated from a Room Database. I receive the updated data from Room as a new list and I then pass it to ListAdapter's submitList to get animations for the changes.
list.observe(viewLifecycleOwner, { updatedList ->
listAdapter.submitList(updatedList)
})
Now, I want to add a drag and drop functionality for the same RecyclerView. I tried to implement it using ItemTouchHelper. However, the notifyItemMoved() is not working as ListAdapter updates its content through the submitList().
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
val list = itemListAdapter.currentList.toMutableList()
Collections.swap(list, from, to)
// does not work for ListAdapter
// itemListAdapter.notifyItemMoved(from, to)
itemListAdapter.submitList(list)
return false
}
The drag and drop now works fine but only when dragged slowly, when the dragging gets fast enough, I get different and inconsistent results.
What could be the reason for this? What is the best way that I can achieve a drag and drop functionality for my RecyclerView which uses ListAdapter?
So I made a quick test (this whole thing doesn't fit in a comment so I'm writing an answer)
My Activity contains the adapter, RV, and observes a viewModel. When the ViewModel pushes the initial list from the repo via LiveData, I save a local copy of the list in mutable form (just for the purpose of this test) so I can quickly mutate it on the fly.
This is my "onMove" implementation:
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
list[from] = list[to].also { list[to] = list[from] }
adapter.submitList(list)
return true
I also added this log to verify something:
Log.d("###", "onMove from: $from (${list[from].id}) to: $to (${list[to].id})")
And I noticed it.. works. But because I'm returning true (you seem to be returning false).
Now... unfortunately, if you drag fast up and down, this causes the list to eventually become shuffled:
E.g.: Let's suppose there are 10 items from 0-9.
You want to grab item 0 and put it between item 1 and 2.
You start Dragging item 0 at position 0, and move it a bit down so now it's between 1 and 2, the new item position in the onMove method is 1 (so far, you're still dragging). Now if you slowly drag further (to position 2), the onMove method is from 1 to 2, NOT from 0 to 2. This is because I returned "true" so every onMove is a "finished operation". This is fine, since the operations are slow and the ListAdapter has time to update and calculate stuff.
But when you drag fast, the operations go out of sync before the adapter has time to catch up.
If you return false instead (like you do) then you get various other effects:
The RecyclerView Animations don't play (while you drag) since the viewHolders haven't been "moved" yet. (you returned false)
The onMove method is then spammed every time you move your finger over a viewHolder, since the framework wants to perform this move again... but the list is already modified...
So you'd get something like (similar example above, 10 items, moving the item 0)> let's say each item has an ID that corresponds to its position+1 (in the initial state, so item at position 0 has id 1, item at position 1 has id 2, etc.)
This is what the log shows while I slowly drag item 0 "down":
(format is `from: position(id of item from) to: position(id of item to)
onMove from: 0 (1) to: 1 (2) // Initial drag of first item down to 2nd item.
onMove from: 0 (2) to: 1 (1) // now the list is inverted, notice the IDs.
onMove from: 0 (1) to: 1 (2) // Back to square one.
onMove from: 0 (2) to: 1 (1) // and undo-again...
I just cut it there, but you can see how it's bouncing all over the place back and forth. I believe this is because you return false but modify the list behind the scenes, so it's getting confused. on one side of the equation the "data" says one thing, (and so does the diff util), but on the other, the adapter is oblivious to this change, at least "yet" until the computations are done, which, as you guessed, when you drag super fast, is not enough time.
Unfortunately, I don't have a good answer (today) as to what would the best approach be. Perhaps, not relying on the ListAdapter's behavior and implementing a normal adapter, where you have better list/source control of the data and when to call submitList and when to simply notifyItemChanged or moved between two positions may be a better alternative for this use-case.
Apologies for the useless answer.
I ended up implementing a new adapter and use it instead of ListAdapter, as mentioned on Martin Marconcini's answer. I added two separate functions. One for receiving updates from Room database (replacement for submitList from ListAdapter) and another for every position change from drag
MyListAdapter.kt
class MyListAdapter(list: ArrayList<Item>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// save instance instead of creating a new one every submit
// list to save some allocation time. Thanks to Martin Marconcini
private val diffCallback = DiffCallback(list, ArrayList())
fun submitList(updatedList: List<Item>) {
diffCallback.newList = updatedList
val diffResult = DiffUtil.calculateDiff(diffCallback)
list.clear()
list.addAll(updatedList)
diffResult.dispatchUpdatesTo(this)
}
fun itemMoved(from: Int, to: Int) {
Collections.swap(list, from, to)
notifyItemMoved(from, to)
}
}
DiffCallback.kt
class DiffCallback(
val oldList: List<Item>,
var newList: List<Item>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return compareContents(oldItem, newItem)
}
}
Call itemMoved every position change:
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
itemListAdapter.itemMoved(from, to)
// Update database as well if needed
return true
}
When receiving updates from Room database:
You may also want to check if currently dragging using onSelectedChanged if you are also updating your database every position change to prevent unnecessary calls to submitList
list.observe(viewLifecycleOwner, { updatedList ->
listAdapter.submitList(updatedList)
})
I've tried danartillaga's answer and got a ConcurrentModificationException for the list variable. I use LiveData in the code and it looks like the data was changed during invalidation of the list.
I've tried to keep the ListAdapter implementation and concluded to the following solution:
class MyListAdapter : ListAdapter<Item, RecyclerView.ViewHolder>(MyDiffUtil) {
var modifiableList = mutableListOf<Item>()
private set
fun moveItem(from: Int, to: Int) {
Collections.swap(modifiableList, to, from)
notifyItemMoved(from, to)
}
override fun submitList(list: List<CourseData>?) {
modifiableList = list.orEmpty().toMutableList()
super.submitList(modifiableList)
}
}
and the onMove code from ItemTouchHelper.SimpleCallback:
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val adapter = recyclerView.adapter as CoursesDownloadedAdapter
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
val list = adapter.modifiableList
// Change your DB here
adapter.moveItem(from, to)
return true
}
The magic here is saving the modifiableList inside the adapter. ListAdapter stores a link to the list from submitList call, so we can change it externally. During the Drag&Drop the list is changed with Collections.swap and RecyclerView is updated with notifyItemMoved with no DiffCallback calls. But the data inside ListAdapter was changed and the next submitList call will use the updated list to calculate the difference.
I'm building a contact list (ArrayList) in a messaging app using RecyclerView.
When a contact updates, I want to move that user to the top of the list, while all other items in the list move one step down.
I'm using Firestore to get the list of users.
DocumentChange.Type.MODIFIED -> {
val matchThatChanged = dc.newIndex
matchesArrayList[matchThatChanged] = ChatMatchListMatch(
matchUserID)
adapter.notifyItemChanged(matchThatChanged) //Ensures change is visible immediately
val fromPosition = matchesArrayList.indexOfFirst {
it!!.matchUserID == matchUserID
}
Log.d(TAG, "From position A: $matchThatChanged")
if (fromPosition != 0) {
adapter.moveMatchToTop(
fromPosition, ChatMatchListMatch(
matchUserID
)
)
}
}
In the Log here it correctly outputs 1 when I make the first move. However, thereafter it outputs 0 when I try to update the other user? This is not correct. The user that gets pushed down should no longer be at 0, it should be at 1. Because it is now wrongly at 0, it is not running the code (this is not the issue. The issue is that is should not be 0 in the first place).
Here's the code in my adapter:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.chat_matchlist_item, parent, false)
return CustomViewHolder(view)
}
// Passes the ContactListMatch object to a ViewHolder so that the contents can be bound to UI.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val contactListMatch = mMatchesArrayList[position]
(holder as CustomViewHolder).bind(contactListMatch)
}
fun moveMatchToTop(fromPosition: Int, chatMatchListMatch: ChatMatchListMatch) {
mMatchesArrayList.removeAt(fromPosition)
notifyItemChanged(fromPosition)
notifyItemRemoved(fromPosition)
mMatchesArrayList.add(0, chatMatchListMatch)
notifyItemInserted(0)
notifyItemChanged(0)
notifyItemChanged(1)
}
The list I have to begin with is:
When I update only the bottom user (2), it displays correctly (moving user two to the top and the other user down):
I now try to update original user 1 again, and I expect it to go on top again, like this:
But instead I get this:
I figured out the solution. I had to make several changes, but I'll try to make this helpful to others who may struggle with similar issues:
I used an index in Firestore and used two "orderby" queries to get the list in the order I wanted:
myCollectionRef
.orderBy("unread", Query.Direction.DESCENDING)
.orderBy("timeStamp", Query.Direction.DESCENDING)
.addSnapshotListener
Then under case MODIFIED:
DocumentChange.Type.MODIFIED -> {
I had to get the old index and the new index of the item changed. When I get the old index (by iterating through my list to look for it with a for loop), I ensure to remove the item from the list and notify my adapter that I removed the item:
val newIndex = dc.newIndex
var oldIndex = -1
for (i in myList.indices.reversed()) {
if (myList[i]!!.IDnumber == userID) {
oldIndex = i
myList.removeAt(oldIndex)
adapter.notifyItemRemoved(oldIndex)
}
}
Then I have to add the changed item back into my list in the correct position. Since I have the new index, this is easy. I also notify my adapter of the change:
myList.add(
newIndex, MySpecialItem(
userID!!,
userImageOrWhatever!!
)
)
adapter.notifyItemInserted(newIndex)
and that's it.
I have a RecyclerView that uses a PagedListAdapter which fetches data from ROOM and Network API. It uses a BoundaryCallback to make request to the API of which the data returned is inserted into the Database(ROOM)
I have a list item that has an increment and decrement button...
The current list is filterable, e.g i can filter the list of products by multiple categories
Problem
If i increase a product item quantity using the increment button, to e.g 12 and then i try filter the list by adding more category, the current list doesn't refresh which is fine because the DiffUtil.ItemCallback confirms that the items are the same, but once i try to increment that same product's quantity after filtering by more category, it starts from zero again....
Note that quantity is not a column in room, it's an Ignored variable.
So i'm not really sure what the problem really is: below is the code that does the increment and decrement.
override fun onIncrementQuantity(position: Int, item: ProductEntity) {
item.quantity = item.quantity + 1
selectedProducts[item.item.id!!] = item
productAdapter?.notifyItemChanged(position, item)
}
override fun onDecrementQuantity(position: Int, item: ProductEntity) {
item.quantity = if (item.quantity == 0) 0 else item.quantity - 1
if (item.quantity == 0) {
selectedProducts.remove(item.item.id)
} else {
selectedProducts[item.item.id!!] = item
}
productAdapter?.notifyItemChanged(position, item)
}
So after hours of debugging here is what i found out.
List is initially loaded with data
updates are done on the current data e.g increment item.quantity = 2
Once new data is fetched (through search or filters), onBindViewHolder is NOT called if the itemsAretheSame or the ContentsAreThemSame thus no need to update the view. Everything at this point is fine.
However, it seems the currentList has been updated to the newly fetched data without necessarily updating the view... Now, when notifyItemChanged is called, onBindViewHolder is triggered:
This is how my onBindViewHolder is
override fun onBindViewHolder(holder: ViewHolder<T>, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
getItem(position) gives you the item from the current list which has been updated and quantity is back to default 0.
Solution
Override the below function of the RecyclerView.Adapter
override fun onBindViewHolder(
holder: ViewHolder<ProductEntity>,
pos: Int,
payloads: MutableList<Any>
) {
if(payloads.isEmpty()) return super.onBindViewHolder(holder, pos, payloads)
val product = getItem(pos)?: return
payloads.forEach {
val oldProduct = it as ProductEntity
if(product.item.id == oldProduct.item.id){
product.quauntity = oldProduct.quantity
}
}
}
The payload should include the partial old data which can be used to update the new data..
According to the documentation
* The payloads parameter is a merge list from {#link #notifyItemChanged(int, Object)} or
* {#link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty,
* the ViewHolder is currently bound to old data and Adapter may run an efficient partial
* update using the payload info. If the payload is empty, Adapter must run a full bind.
* Adapter should not assume that the payload passed in notify methods will be received by
* onBindViewHolder(). For example when the view is not attached to the screen, the
* payload in notifyItemChange() will be simply dropped.
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