How to set scroll position when using RecyclerView with FirestoreRecyclerAdapter? - android

I'm using a RecyclerView to display some data from a Firestore database. I'm using as an adapter, the FirestoreRecyclerAdapter for obvious reasons. I'm successfully displaying all 35 items in my RecyclerView. The problem is, I cannot scroll to a specific position. This is what I have tried:
recyclerView = locationsFragmentView.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
FirestoreRecyclerOptions<MyModelClass> options = new FirestoreRecyclerOptions.Builder<MyModelClass>().setQuery(query, MyModelClass.class).build();
adapter = new MyFirestoreRecyclerAdapter(options);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(false);
recyclerView.getLayoutManager().scrollToPosition(10);
Everytime I open my app, I'm always positioned at the first position and not on the 10'th as I specified in the scrollToPosition() method.
I have also used:
recyclerView.scrollToPosition(10);
and
recyclerView.smoothScrollToPosition(10);
But without luck. How do I scroll to specific position? Thanks!

The recyclerView doesn't scroll because it's still empty when you call recyclerView.scrollToPosition(10);, you should move that code after the recyclerView gets populated from the Firebase response, probably inside a callback.

Instead of using recyclerView.scrollToPosition(10); try to use below code to set position in recyclerview
recyclerView.smoothScrollToPosition(10);

In my case, I didn't want move to a specific position, instead I just wanted to keep the original position after returning from a different Activity or returning from background.
I just added this empty listener and it's working.
listAdapter.snapshots.addChangeEventListener(object : ChangeEventListener {
override fun onChildChanged(
type: ChangeEventType,
snapshot: DocumentSnapshot,
newIndex: Int,
oldIndex: Int
) {
}
override fun onDataChanged() {
}
override fun onError(e: FirebaseFirestoreException) {
}
})
Here listAdapter is FirestoreRecyclerAdapter.
Note: There must have some negative consequences on doing this.

I solved my problem with a Simpler solution.
I just called adapter.startListening(); in onViewCreated() instead of onStart() and called adapter.stopListening(); in onDestroyView() instead of onStop()
That prevented the entire list from regenerating while coming back from the next activity and thus retained the scroll position where it was previously.
Source: https://github.com/firebase/FirebaseUI-Android/issues/998#issuecomment-342413662

Related

How to replace adapter list of data without refreshing the recyclerview

i have recyclerView in my fragment and i want to change the list of data in the recyclerView adapter without refreshing the recyclerView
i am using this two functions like that
mAdapter.clearList()
mAdapter.addItems(newList)
but there is a quick refresh because of clearList() function
anyone have better function to use
fun addItems(items: List<T>) {
val myList = adapterItems()
var count = myList.size
// Remove loading indicator dummy item
if (count > 0 && hasMore()) {
count--
myList.removeAt(count)
notifyItemRemoved(count)
}
// Insert extra data
myList.addAll(items)
notifyItemRangeInserted(count, items.size)
}
fun clearList() {
val myList = adapterItems()
val count = myList.size
myList.clear()
notifyItemRangeRemoved(0, count)
}
You could make a setItems function that clears and then adds before notifying the RecyclerView with notifyDataSetChanged() - I'm not sure if that would matter though, I feel like it should all be resolved before you get a layout pass and the screen updates. A glitch you can see feels like one thing happens and then another later - I'm not sure how it's all handled internally though, if these things are queued up or not. Worth a try!
Another thing you could try is using notifyDataSetChanged() in clearList() instead of notifyItemRangeRemoved. Since you're throwing out everything it makes more sense to just use the "everything has changed" notify call. I'm not sure if using a rangeRemoved call followed by a rangeInserted one might cause problems - it shouldn't, but depending on how it's resolved, there could be some quirks.
The other thing to check is that you're definitely only updating your adapter once - if whatever's causing that update fires twice in a short space of time, you could see it glitch

RecyclerView with RoomDatabase an ViewModel

I'm showing my recycler view like this:
parrafoLeyViewModel.allParrafosLey.observe(this, Observer {
recyclerview_lectura_ley.layoutManager = LinearLayoutManager(this)
adapterListar = AdapterListarLey(this, it, this)
recyclerview_lectura_ley.adapter = adapterListar
})
So when I insert data into the RoomDatabase my recyclerview restarts and goes back to the start
GlobalScope.launch {
db.parrafoLeyDao().updateComentarioLey(
ComentarioLey(
idParrfo,
spTipoComentario.selectedItem.toString(),
etInserteComentario.text.toString()
)
)
}
What I want is that when I insert the data it remains in the same position. I am using ViewModel
You are not correctly passing the data to Recycler View
Please make sure your Data Source is wrapped inside a Live Data
And then pass this live data to your Recycler View Adapter.
This way Recycler View will only re-draw the changed item and not the entire list. Because Recycler View works well with Live Data.
Any method of Recycler View which will cause the list to re-draw should be excluded.
for e.g. notifyDataSetChanged() method.
Instead call submitList()
And lastly please go through below documentation:
Recycler View Sample
RecyclerView.State

Android Repository pattern - how to update a nested recycler view item from view model

I am using ViewModel and repository pattern to fetch the data in the list. The items are arranged as a list of rows and products. A Row class has products in it. The products can be scrolled horizontally. I am using a recycler view with linear layout manager (horizontal orientation) nested inside another recycler view (vertical orientation). Fetching the items via a ViewModel and rendering in the recycler views is pretty straightforward. The challenge is when I try to update the items (their counts) when an item is added to the cart. When the button (plus sign) is clicked, a callback is sent to the view model via a listener. The horizontal adapter sends the request back to the container (vertical) adapter, and the vertical adapter sends it back to the view model.
// The horizontal adapter
class SimpleProductAdapter(
private val shopId: String,
private val listener: (product: CartProduct) -> Unit
) :
ListAdapter<CartProduct, RecyclerView.ViewHolder>(...DiffCallbackGoesHere) {
// ... some more things here
fun bind(item: CartProduct?) {
view.add_to_cart_button.setOnClickListener {
listener(item)
}
}
The vertical adapter has similar structure
class RowAdapter(
private val shopId: String,
private val listener: (product: CartProduct) -> Unit
) :
PagedListAdapter<Row, RecyclerView.ViewHolder>(...RowDiffCallbackGoesHere) {
// ... some more things here
fun bind(item: Row?) {
SimpleProductAdapter(shopId) { product ->
listener(product)
}
}
And the main site inside the fragment where the viewmodel calls exist:
val rowAdapter = RowAdapter(args.shopId) { product->
if (actionType == ADD_TO_CART_ACTION) viewModel.buy(product)
.observe(viewLifecycleOwner, Observer {
view.swipe.isRefreshing = it is Resource.Loading
// I want to update the quantity here on success result
if(Resource is Success) {}
})
When the result is Success, I want to update the quantity; two things are challenging here
I am using PagedListAdapter from the paging library which gives me a (supposedly) immutable list of products.
Even if I update the PagedList and issue a notifyDataSetChanged, it would be too much to do that just to change a single count over a large set of items.
I am hoping to find a way where I can easily target the specific product to update or another alternative that I keep seeing on the web is to build a custom layout manager so that I can have a single adapter that draws everything on a single pass without having to nest the recycler views. That way updating the item would become easier (couldn't find a code example on this).
Any suggestions please.
In Paging3, there are plans to eventually add an API to support granular updates without invalidation: https://issuetracker.google.com/160232968
For now, you must invalidate in order to update the backing dataset. In general, DiffUtil will do a pretty good job of hiding this from the user though.

My Recyclerview cannot scroll until getting all the data from firebase storage

I am here to demonstrate what my problem is
First, my app is using MVVM architecture .
I have a repository to fetch all list file from firebase storage
fun getRestItem(type : String) : LiveData<ArrayList<eachItem>> {
val list = ArrayList<eachItem>()
val listStream = MutableLiveData<ArrayList<eachItem>>()
storage.child(type).listAll().addOnSuccessListener {
it.items.forEach { item ->
item.downloadUrl.addOnSuccessListener { url ->
val eachItem = eachItem(url.toString())
list.add(eachItem)
listStream.value = list
}
}
}
return listStream
}
And viewmodel to get data
fun getItem() : LiveData<ArrayList<eachItem>> {
return drawRepository.getRestItem("Item")
}
Finally , the UI observe the data
model.getItem().observe(viewLifecycleOwner, Observer {
if (it != null) {
val adapter = ItemAdapter()
adapter.submitList(it)
binding.doilyRecycler.adapter = adapter
}
})
My adapter is using ListAdapter class .
The problem is while we are fetching the data , the recyclerview (UI) keeps back to the top and cannot be scrolled down , even though I force to scroll down , the view will keep back to the top until all data from storage has been fetched .
I really don't know what the issue here , I have used listAdapter for so many times and I didn't face such a problem before (Maybe It's because the firebase storage (the way I fetch the data) ?). The scrollbar i can see it is changing while the data size becomes bigger and bigger
I am guessing that the recycerview keep refreshing while new data is comming ...
Can you guys give me so suggestions ?
*** edit (show my recyclerview xml
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/doilyRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:spanCount="3"
android:clipToPadding="false"
tools:listitem="#layout/each_item"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager">
</androidx.recyclerview.widget.RecyclerView>
you can try using test list data,not from firebase storage,if question still exist then explain firebase storage is work
I simply fixed the problem by wrapping the recyclerview with nestScrollview , it works (But it really lags while scrolling and getting the data at the same time, it is unacceptable).
I don't know really know why the recyclerview scroll behavior like this in this situation .

ListAdapter not updating item in RecyclerView

I'm using the new support library ListAdapter. Here's my code for the adapter
class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent.inflate(R.layout.item_artist))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(artist: Artist) {
itemView.artistDetails.text = artist.artistAlbums
.plus(" Albums")
.plus(" \u2022 ")
.plus(artist.artistTracks)
.plus(" Tracks")
itemView.artistName.text = artist.artistCover
itemView.artistCoverImage.loadURL(artist.artistCover)
}
}
}
I'm updating the adapter with
musicViewModel.getAllArtists().observe(this, Observer {
it?.let {
artistAdapter.submitList(it)
}
})
My diff class
class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem?.artistId == newItem?.artistId
}
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
}
What's happening is when submitList is called the first time the adapter renders all the items, but when submitList is called again with updated object properties it does not re-render the view which has changed.
It re-renders the view as I scroll the list, which in turn calls bindView()
Also, I've noticed that calling adapter.notifyDatasSetChanged() after submit list renders the view with updated values, but I don't want to call notifyDataSetChanged() because the list adapter has diff utils built-in
Can anyone help me here?
Edit: I understand why this happens that wasn't my point. My point is that it at least needs to give a warning or call the notifyDataSetChanged() function. Because apparently I am calling the submitList(...) function for a reason. I am pretty sure people are trying to figure out what went wrong for hours until they figure out the submitList() ignores silently the call.
This is because of Googles weird logic. So if you pass the same list to the adapter it does not even call the DiffUtil.
public void submitList(final List<T> newList) {
if (newList == mList) {
// nothing to do
return;
}
....
}
I really don't understand the whole point of this ListAdapter if it can't handle changes on the same list. If you want to change the items on the list you pass to the ListAdapter and see the changes then either you need to create a deep copy of the list or you need to use regular RecyclerView with your own DiffUtill class.
The library assumes you are using Room or any other ORM which offers a new async list every time it gets updated, so just calling submitList on it will work, and for sloppy developers, it prevents doing the calculations twice if the same list is called.
The accepted answer is correct, it offers the explanation but not the solution.
What you can do in case you're not using any such libraries is:
submitList(null);
submitList(myList);
Another solution would be to override submitList (which doesn't cause that quick blink) as such:
#Override
public void submitList(final List<Author> list) {
super.submitList(list != null ? new ArrayList<>(list) : null);
}
Or with Kotlin code:
override fun submitList(list: List<CatItem>?) {
super.submitList(list?.let { ArrayList(it) })
}
Questionable logic but works perfectly.
My preferred method is the second one because it doesn't cause each row to get an onBind call.
with Kotlin just you need to convert your list to new MutableList like this or another type of list according to your usage
.observe(this, Observer {
adapter.submitList(it?.toMutableList())
})
I had a similar problem but the incorrect rendering was caused by a combination of setHasFixedSize(true) and android:layout_height="wrap_content". For the first time, the adapter was supplied with an empty list so the height never got updated and was 0. Anyway, this resolved my issue. Someone else might have the same problem and will think it is problem with the adapter.
If you encounter some issues when using
recycler_view.setHasFixedSize(true)
you should definitly check this comment:
https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531
It solved the issue on my side.
(Here is a screenshot of the comment as requested)
According to the official docs :
Whenever you call submitList it submits a new list to be diffed and displayed. This is why whenever you call submitList on the previous (already submitted list), it does not calculate the Diff and does not notify the adapter for change in the dataset.
Wasted so much time to figure out the problem in same case.
But in my situation the problem was that i forgot to specify a layoutManager for my recyclerView: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())
I hope no one will repeat my mistake...
Today I also stumbled upon this "problem".
With the help of insa_c's answer and RJFares's solution I made myself a Kotlin extension function:
/**
* Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
*
* Originally, [ListAdapter] will not update the view if the provided list is the same as
* currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
* could never work - the [ListAdapter] must have the previous list if items to compare new
* ones to using provided diff callback.
* However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
* the view to be updated. This extension function handles this case by making a copy of the
* list if the provided list is the same instance as currently loaded one.
*
* For more info see 'RJFares' and 'insa_c' answers on
* https://stackoverflow.com/questions/49726385/listadapter-not-updating-item-in-reyclerview
*/
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
// ListAdapter<>.submitList() contains (stripped):
// if (newList == mList) {
// // nothing to do
// return;
// }
this.submitList(if (list == this.currentList) list.toList() else list)
}
which can then be used anywhere, e.g.:
viewModel.foundDevices.observe(this, Observer {
binding.recyclerViewDevices.adapter.updateList(it)
})
and it only (and always) copies the list if it is the same as currently loaded one.
In my case I forgot to set the LayoutManager for the RecyclerView. The effect of that is the same as described above.
I got some strange behavior. I'm using MutableList in LiveDate.
In kotlin, the following codes don't work:
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it)
})
But, when I change it to it.toList(), it works
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it.toList())
})
Although, "it" was the same list.
For me, this issue appeared if I was using RecyclerView inside of ScrollView with nestedScrollingEnabled="false" and RV height set to wrap_content.
The adapter updated properly and the bind function was called, but the items were not shown - the RecyclerView was stuck at its' original size.
Changing ScrollView to NestedScrollView fixed the issue.
I had a similar problem. The issue was in the Diff functions, which didn't adequately compare the items. Anyone with this issue, make sure your Diff functions (and by extension your data object classes) contain proper comparison definitions - i.e. comparing all fields which might be updated in the new item. For example in the original post
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
This function (potentially) does not do what it says on the label: it does not compare the contents of the two items - unless you have overridden the equals() function in the Artist class. In my case, I had not, and the definition of areContentsTheSame only checked one of the necessary fields, due to my oversight when implementing it. This is structural equality vs. referential equality, you can find more about it here
The reason your ListAdapter .submitlist is not called is because the object
you updated still holds the same adress in memory.
When you update an object with lets say .setText it changes the value in the original object.
So that when you check if object.id == object2.id it will return as the same
because the both have a reference to the same location in memory.
The solution is to create a new object with the updated data and insert that in your list. Then submitList will be called and it will work correctly
It solve my problem. I think the best way is not to override submitList but add a new function to add new list.
fun updateList(list: MutableList<ScaleDispBlock>?) {
list?.let {
val newList = ArrayList<ScaleDispBlock>(list)
submitList(newList)
}
}
I also ran into similar issue, my usecase was i had a clickHandler and item will be selected/not selected (toggle on click).
I tried most of the approach from the above answers, only thing that worked is
adapter.submitList(null)
adapter.submitList(modifiedList)
but problem with this is everytime i click on any clickHandler the whole list is being redrawn again which is very ineffecient.
What i did ?
I made a live data that will store last clicked item and observing that live data, we can tell adapter that live data has been updated like below
viewModel.lastClicked.observe(viewLifeCycleOwner, {
adapter.notifyItemChanged(it)
}
Had a VERY similar issue, to this one, and decided to open a new thread and even create a GitHub project to mess around with. Most solutions didn't quite work for me, not even the toMutableList() way. In my case, the problem was solved by using immutable classes and submitting immutable Lists to the Adapter.
For anyone who's scenario is same as mine, I leave my solution, which I don't know why it's working, here.
The solution which worked for me was from #Mina Samir, which is submitting the list as a mutable list.
My Issue scenario :
-Loading a friend list inside a fragment.
ActivityMain attaches the FragmentFriendList(Observes to the livedata of friend db items) and on the same time, requests a http request to the server to get all of my friend list.
Update or insert the items from the http server.
Every change ignites the onChanged callback of the livedata. But, when it's my first time launching the application, which means that there was nothing on my table, the submitList succeeds without any error of any kind, but nothing appears on the screen.
However, when it's my second time launching the application, data are being loaded to the screen.
The solution is, as metioned above, submitting the list as a mutableList.
As has already been mentioned, you cannot submit a List with the same reference because the ListAdapter will see the lists are in the same location and will therefore not be able to use the DiffUtil.
The simplest solution would be to make a shallow copy of the list.
submitList(ArrayList(list))
Be wary converting the List to a MutableList, as that can create conditions for Exceptions and hard to find bugs.
this will work ....
what happen Is when you get the current list you are pointing to the same list at same location
I needed to modify my DiffUtils
override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
To actually return whether the contents are new, not just compare the id of the model.
Using #RJFares first answer updates the list successfully, but doesn't maintain the scroll state. The entire RecyclerView starts from 0th position. As a workaround, this is what I did:
fun updateDataList(newList:List<String>){ //new list from DB or Network
val tempList = dataList.toMutableList() // dataList is the old list
tempList.addAll(newList)
listAdapter.submitList(tempList) // Recyclerview Adapter Instance
dataList = tempList
}
This way, I'm able to maintain the scroll state of RecyclerView along with modified data.
Optimal Soltion:
for Kotlin
var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
list.add(Item("Content"))
adapter.submitList(list) {
Log.e("ListAdaptor","List Updated Successfully")
}
We should not maintain another base list as adapter.currentList will return a list in which diff is already calculated.
We have to provide a new instance every time a list updated because of DiffUtil
As per android documentation
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.
One list is already maintained by AsyncListDiffer which runs the diffutil on the background thread and another one has to be passed using adaptor.submitList()
The way that worked for me is to override the submitList() and create a copy of the incoming list and each item inside it too:
override fun submitList(list: List<Item>?) {
val listCopy =
mutableListOf<Item>().apply {
list?.map {
add(Item(it.id, it.name, it.imageUrl))
}
}
super.submitList(listCopy)
}
I encounter a very similar issue.
After the data list changed, I submit it again, the recycler view doesn't show as I wanted. It shows duplicated items.
I haven't found the root cause, but I find a workaround, that is to set the adapter to recycler view again. I guess this makes recycler viewer forget the memory before and render again correctly.
userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)
Once you have modify the array list, you have to let adapter know that which position that should be change
this code below is working in my case wish it may help
private fun addItem() {
val index = myArrayList.size
val position = myArrayList.size+1
myArrayList.add(
index, MyArrayClass("1", "Item Name")
)
myAdapter.notifyItemInserted(position) // in case of insert
// in case of remove item
// val index = myArrayList.size-1
// myAdapter.notifyItemRemoved(index)
}
just call adapter.notifyDataSetChanged() after differ.submitList
In my case i was using same object(from adadptar) to update Room database.
Create new object to update database and it'll fix the issue.
Example: I was doing this ->
val playlist = adapter.getItem(position)
playlist.name = "new name"
updatePlaylistObjectInRoomDatabase(playlist)
above code will change object in adapter before room database. So no change will be detected by DiffUtil callback.
Now doing this ->
val playlist = adapter.getItem(position)
val newPlaylist = Playlist()
newPlaylist.id = playlist.id
newPlaylist.name = "new name"
updatePlaylistObjectInRoomDatabase(newPlaylist)
Above code will not change anything in adapter list and will only change data in room database. so submitList will have different values DiffUtil callback can detect.
Enjoy the little things :)
This is something naturally expecte to be available on the official API, but as it isn't, this can be a way to deal with it:
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.clearItems() {
submitList(null)
submitList(emptyList())
}
The adapter can not understand that you have some updates, I don't know why!?
I am adding some entities to the list ad I m expected to collect them at the consumption point. But, nothing happens.
As a solution that worked for me you can use the script below:
artistAdapter.submitList(it.toMutableList())
Because the problem lays inside the ListAdapter, I would like to solve it inside the ListAdapter.
Thanks to Kotlin extension, we can write it like:
class MyItemAdapter() :
ListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) {
// ...
override fun submitList(list: List<Item>?) {
super.submitList(list?.toList())
}
}
It does look like a tricky hack. So I'd like to make a comment too:
super.submitList(list?.toList()) // to make submitList work, new value MUST be a new list. https://stackoverflow.com/a/50031492/9735961
And yes, thank you, RecyclerView developers.

Categories

Resources