How to properly use LiveData with RecycleView - android

I have a project that loads a list from the server. This data will eventually be stored into a database, but for now is stored in memory in a MutableLiveData. A RecyclerView's adapter is watching the data and displaying it. So far everything is working as expected, using a FAB the user can post a new entry which will go at the top of the list, on success I get a 200 and here's the main part where I'm getting lost...
When I want to add a single item to a list stored in a LiveData, the observer is unaware of the delta. I currently make a call to RecyclerView.Adapter.notifyDataSetChanged(), though the ideal in my case would be to call notifyItemInserted(0) or in other cases I can see various other notifications. What the best way to do this? The lifecycle architecture library appears to be very well thought of, I assume I'm missing something simple. I can't imagine having to manually perform a diff between the lists?

Related

RecyclerView notifyDataSetChanged() updates both adapters when I call this method on one

I have two adapters:
private StreamingNowAdapter mStreamingNowAdapter;
private UserStreamingNowAdapter mUserStreamingNowAdapter;
When I load them with Arrays
mStreamingNowAdapter = new StreamingNowAdapter(mEventId, mEventName,
mEventPreview, mNumberViewers, mContext);
mUserStreamingNowAdapter = new UserStreamingNowAdapter(mUserId,
mUsername, mProfilePic, mStreamingUserNumberViewers, mContext);
and call notifyDataSetChanged(); how come when I call this method on one adapter, both adapters become notified and displayed?
I only called the method on the first adapter
mStreamingNowAdapter.notifyDataSetChanged();
and it loads my data into my second recycler mUsersStreamingNowAdapter without me calling notifyDataSetChanged on the second adapter.
Is this a bug or is it supposed to be like this?
Edit1: Two separate recyclerviews, both adapters attached to their own recyclers.
RecyclerView mRecyclerViewStreaming =
view.findViewById(R.id.fraghome_recycler_streaming_now);
RecyclerView mRecyclerViewFollowing =
view.findViewById(R.id.fraghome_recycler_followed_users);
mRecyclerViewStreaming.setLayoutManager(new
LinearLayoutManager(getContext()));
mRecyclerViewFollowing.setLayoutManager(new
LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false));
mRecyclerViewStreaming.setAdapter(mStreamingNowAdapter);
mRecyclerViewFollowing.setAdapter(mUserStreamingNowAdapter);
I've checked the layout multiple times to make sure both the recyclerview's id's are not identical. Still can't figure out why this is happening.
A correctly managed RecyclerView and its required artifact (an Adapter, its data, etc.) should not interfere with another correctly managed RecyclerView or any other component for what is worth.
Unfortunately, the shared code you provided is not sufficient to determine what could be happening in your project.
To the best of my knowledge, RecyclerView adapters don't talk to each other in any way so a notifyDataSetChanged() on an adapter, should have no impact whatsoever on another adapter, regardless of where said adapters are declared (assuming they are different instances, of course).
All this being said, I'll offer you my personal comments about the code you posted. Please don't take it personally, for I'm reviewing the code, not the author, from a pure technical point of view based upon my experience with Android.
Fragment
It immediately caught my attention to see that a Fragment, a component known for being completely wild when it comes to lifecycle management and state, has so many responsibilities.
It starts by having no less than eight array lists, and managing two recyclerviews, and its respective adapters, and even the LayoutManagers used by the lists.
Not happy with having to manage all this, this Fragment also needs to handle a networking layer, error handling (not yet implemented), and data storage (in memory for now).
Because you may have put this together really fast simply to illustrate a case, I will let the fragment in peace for now.
Data
You're putting a lot of stress on your fragment and adapters by splitting the data in a very inefficient way. 4 array lists, one per field? What?
for(HomeFragmentPojo.StreamingNow streamingNow : streamingNowArrayList){
mEventId.add(streamingNow.getEvent_id());
mEventName.add(streamingNow.getEvent_name());
mEventPreview.add(streamingNow.getEvent_preview());
mNumberViewers.add(streamingNow.getNumber_viewers());
}
Why are you doing this?
If you have a StreamingNow object that has all 4 fields.
Why don't you have a ListAdapter<StreamingNow, ViewHolder>() that simply takes a list of StreamingNow objects?
You can simply do adapter.submitList(...) to pass the list once you get it and the adapter (when properly created) will do the right thing.
If you ever update the list, you simply submit it again, and the adapter will calculate the difference and only update the rows that need to be updated.
The BIND method, is also simple, because it would look like: (pseudo code)
viewHolder.eventName = getItem(position).name
viewHolder.viewerCount = getItem(position).viewerCount
//For the image...
Glide.with(mContext)
.load(getItem(position).preview)
.apply(RequestOptions.centerCropTransform())
.into(viewHolder.preview);
...
You get the idea.
Based upon the properties I see, your StreamingNow "pojo" kinda looks like that
class StreamingNow {
String id;
String name;
String preview; //maybe an URL? You use Glide so in the bind method, use it.
String viewerCount;
}
When I look at your other adapter, I see that it has the same data...
Id, Name, Pic, Count of Viewers.
There is no need to use a different adapter, you can reuse the same adapter assuming they have to behave the same way; if the Users adapter has to do something different on click, for example, you could pass the min information needed for the adapter to decide what to do when an item is clicked. Or better yet, you could simply pass a generic Listener so the adapter simply calls "this item "X" was clicked". Because these adapters aren't here to make decisions and call framework things, they have a lot of work in their plates, and this is not and should not be their responsibility.
Whoever manages, creates, and maintains these adapters (so far, your Fragment) should be the one in charge of doing this. Or even better yet, a ViewModel or Presenter should receive this and make an informed decision.
In any case, the click event inside the adapter is not the biggest problem here, and it will work anyway.
What I am trying to leave written here, is that your problem is a lack of separation of concerns between components.
The smaller and more concrete your classes are, the easier it is to replace them, test them, debug them, enhance them, etc.
You're throwing all your code in a big place. If you separate it, it's a lot easier to keep it under control.
So, what's the deal?
I suggest you take a step back, enhance your RecyclerView to use a ListAdapter
I wouldn't perform the network request in onCreate, instead do it in onViewCreated or similar, because during onCreate in a Fragment, there's no guarantee that the view will be created. (Remember when I told you Fragments were a pain?).
I wouldn't have that network code in the Fragment to be honest; it's quite simple to add a ViewModel and have the data there. Then you can have the ViewModel request the data during onCreate, but you'll only get the LiveData from it when your lifecycle allows it... which is the whole point of LiveData and observing it.
If this sounds like a lot of work, it may as well be, especially if you are new to Android. But it will pay off.
Good luck!

After DataSource.Invalidate() new PagedList has only one page

I have a list with pagination which I implemented using Paging library. Items on this list can be modified (changed/deleted).
According to official documentation, I'm first changing in-memory list cache from which my DataSource gets pages and after that calling datasource.invalidate() in order to create new pair PagedList/DataSource:
If you have more granular update signals, such as a network API signaling an update to a single item in the list, it's recommended to load data from network into memory. Then present that data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot can be created.
It works and looks WELL if user modifies items on first page.
However, if user is on page two or further during datasource.invalidate() he will be thrown at the end of the first page.
Debugging shows this happens because new PagedList has only first page when it's submitted to PagedListAdapter.submitList. Adapter compares old and new lists and removes all items not from first page. It happens always but not visible for user if he is on the first page.
So to me, it looks like new pair PagedList/DataSource have no idea about number of pages which fetched previous pair and datasource.invalidate() doesn't fit for the situation in docs. Behavior that I see acceptable for cases then user updates all list (like swipe-to-refresh) but not
an update to a single item in the list
Has anybody faced such issue or somehow archived things I want? Maybe I'm missing some trick which helps me to get new PagedList already with all pages.
For clarification: library version 2.1.0. Custom PageKeyedDataSource based on in-memory cache and remote servise (No Room)
I want to share my research in case anybody is interested:
Issue ("lack of feature") is known, at least I've found the couple related discussions on official tracker one two
If you are using PositionalDataSource or ItemKeyedDataSource you should dig into the direction of requestedStartPosition/requestedInitialKey from initial params as this answer says. I didn't have much time to build the whole solution but those params are indeed different for initial load after invalidation
About my case : PageKeyedDataSource. Here you can read that there is no similar to requestedInitialKey params in this type of data source. Still, I found a solution which fits me, very simple, although, feels like a dirty trick:
When loadInitial() is called after invalidate() in-memory cache returns all already loaded pages instead of just first one.
At first I was worry that something will break if, for example, requestedLoadSize is 5 but the result is 50 items list but turns out it's just a hint and it can be ignored. Just don't forget to pass nextPageKey which corresponds to the last cached page and not the first one.
Hope it will help
With observable method you will only get first page list items....if you want to edit other items you can get that list by adapter.currentlist method.
Example:
private fun list():MutableList<String>{
val list = mutableListOf<String>()
for (value in videosAdapter.currentList.orEmpty()) {
val abc = value.snippet.resourceId.videoId
list.add(abc)
}
return list
}

Paging Library in Chat App

According to a response made by Yigit Boyar from Google, Live Data is not the best use case for a chat app, because it may lose displaying some items if they come at the same time. He recommends using the new Google's Paging Library. I've been using ItemKeyedDataSource for my inbox(all the people that have message the user), and the inside chat(the messages themselves). The problems are the following:
1- From the chat, when the user scrolls downwards, the user retrieves old messages, which means that the insertion of those messages should be in position 0 of the adapter, not sequentially like the paging library does. How can I alternate the position of the inserted items to be sequentially for new messages, and in position 0 for old messages?
2- From the inbox(people that have message the user), again I'm using ItemKeyedDataSource here, the problem is that I want to maintain the multiple document listener from the repository (I'm using Firebase Firestore), so I can detect every time a new person talks to the user. The problem is that callback.onResult is called only once, and fails when Firebase sends another user. How can I maintain an update-able list?
I understand that this answer is probably too late, but maybe it can help someone in future.
Position of item in RecyclerView is determined by the position of corresponding data object (of type T) inside PagedList<T>. PagedList is designed to look alike good old List<T>, but can be thought of as an "endless" list of elements.
PagedList gets its elements by pages on demand through something called DataSource.Factory. A Factory is used because DataSource by itself can only grow in one direction. If you need to prepend elements in PagedList, or change or remove existing elements you must invalidate the DataSource and a new instance will be created through DataSource.Factory.
So, to insert your data elements where you want them you should implement your own DataSource and DataSource.Factory by subclassing these base classes.
Note: Room, data persistence library from AndroidX, provides facilities to automatically generate instances of these classes for your data. You can write SQL query like this:
SELECT * FROM messages WHERE threadId=:threadId ORDER BY timestamp DESC
then get DataSource.Factory from this, use the factory to create LivaData<PagedList<Message>> and finally use the paged list to display messages in a RecyclerView in a chat application. Then, when you insert, update or remove data inside DB these changes will automatically propagate to the UI. This can be very useful.
I recommend you to read a few related examples a do codelabs:
https://codelabs.developers.google.com/codelabs/android-paging/#0
https://github.com/googlesamples/android-architecture-components/tree/master/PagingSample
https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample

Is it possible to get only changed data with query observe in ObjectBox?

I was reading the doc about observing queries.
Query<Task> query = taskBox.query().equal(Task_.completed, false).build();
subscription = query.subscribe().observer(data -> updateUi(data));
From what i understand , this code returns all the data every time. but for RecyclerView add/remove animation to work, we need to know which data is changed and we need to know what kind of change is happened to data (remove/change/add).
is there anyway to get changed data only?
It's not a responsibility of ObjectBox to define a change. There is DiffUtil that responsible for that in android. If you google that you can find tons of examples (e.g. sample). The only advice there is to put DiffUtil payload to background thread if your list contains lots of items or items are fat(contain dozens of fields).

update listview cells/items whose index may change

I have elements in a listview that change the way they look based on a network response
by the time the network responds the listview item (or item in the arraylist) could be at a different index
What I can do:
Make an alternate api call back to the server which returns all the items in the list (in their most updated form), and then call notifyDataSetChanged() on the adapter
but this seems like a waste of processes, and so does some alternative of searching an arraylist for the equivalent object, updating it and then calling notifyDataSetChanged()
Is there a way instead to have something like a BroadcastReceiver within the adapter that can keep track of the adapter item which started the network call or service? any maybe only respond to the receiver if the view is not currently recycled
It's hard to give an exact answer as your best approach since what you described is a really high level overview. I'll have to give an equal high level answer. Hopefully it help.
There's not many ways around searching an ArrayList in the adapter for a given item. One good idea:
You could create a custom adapter which is backed by an ArrayList but also maintains a Set of the data as well. The benefit is finding an item is O(1) however any adds or removes require you to modify two collections instead of one...which will cause a slight slow down. I've personally had to use this solution once for a highly complex adapter/listview. It could get updated quite often (to the point throttling notifyDataSetChanged() was once discussed) Surprisingly the slow down in maintaining a List and Set was hardly noticeable and overall worked well.
You could use a similar approach if your data has some sort of unique id associated with it. In which case you could build a Map of the data and use the maps values() method to obtain the List to use for the adapter. While using the keys to quickly find and update the required data. This may or may not be more difficult then the Set idea. Further if you can get your data into a SparseArray (having a unique int for each item), then you could use a SparseArrayAdapter which can get you O(log n) search times. Of course you loose the ability to sort your data in any meaningful way.
I'm not sure how viable the BroadcastReceiver idea is. I would see it more like each item's object instance would control the network request/response for itself, but that would seem tricky and odd. There's always the option of using a CursorAdapter. Just store all your data to DB. Have the network calls update the DB which can then be reflected within the CursorAdapter.

Categories

Resources