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

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
}

Related

DataSource - Android Paging with dynamically growing list

I'm trying to implement paging on Android using a DataSource implementation, plus PagedAdapter.
Initially, the requirements were to have a full list in memory (not using RoomDB) and I wanted to take a moving "view" over that data as the user scrolls - i.e. feeding it to the adapter in pages. I've accomplished that with the use of PositionalDataSource.
However, now I have a new requirement. Some of the items in the original full list are actually "loading" items (i.e. spinners) and I need to fetch the data that these cells represent in chunks. These chunks have undetermined sizes. When a chunk loads in, the "loading" item should move down the list, and the loaded chunk be inserted where the "loading" item used to be. This should continue until all chunks that the "loading" item represents have been loaded in, at which point the "loading" item at the end of the list should be removed.
This means, my underlying data source actually grows dynamically as the user scrolls through the list. Which I think means PositionalDataSource is not the right type of data source to use as the source docs state:
* Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
* arbitrary page positions.
Emphasis on fixed-size and countable - obviously my data set is not fixed size (and is therefore also uncountable).
I've looked at other implementations of DataSource and think I've found the right one; ItemKeyedDataSource. Each of my items do indeed have a unique key, and the source docs of this class state that:
* Incremental data loader for paging keyed content, where loaded content uses previously loaded
* items as input to future loads.
Which to me indicates that I can use it for my required purposes. I.e. when it needs to load in a range for an item with the given key which also happens to be a "loading" item, it can use the loading items data to determine what to load.
However, I'm struggling a bit with actual implementation of this, as the official docs don't give any real example usage, and the links to example code assumes the use of RoomDB or retrofit, neither of which is the approach I need.
Could anyone help with giving me an overview of how this DataSource is supposed to function conceptually and/or in code examples using an in memory data set that needs to grow dynamically?
I realize this is pretty vague, I've only started working with this class this morning and I'm struggling.
Paging already loads paginated data for you - the point of implementing a DataSource is to provide Paging a way to incrementally load more data as user scrolls near the end. The one caveat is that in Paging 2.x, load state is not built into the library so you need to track it yourself and show the spinner using some method such as ConcatAdapter.
If you want to try the v3 apis (still in beta), LoadState is a built-in concept and you can simply use the .withLoadStateFooter() transform to turn a PagingDataAdapter to a ConcatAdapter which automatically shows the loading spinner when Paging fetches a new page.
To clarify the bit on the docs about counted snapshots - Paging operates with a single source of truth (DataSource / PagingSource), which is supposed to represent a static list (once fully loaded). This doesn't mean you have to have the whole list in memory, but the items each instance of DataSource fetches should generally match the mental model of a static list. For example if you are paging in data from DB, then a single instance of DataSource / PagingSource is only valid while there are no changes in the DB. Once you insert / modify / delete a row, that instance is no longer valid, which is where DataSource.Factory comes into play, giving you a new PagedList / DataSource pair.
Now if you need to also incrementally update the backing dataset (DB in this example) via layered source approach, Paging v2 offers a BoundaryCallback you can register to fire off network fetch when Paging runs out of data to load, or alternatively in v3 the new API for this is RemoteMediator (still experimental).

But really, how does the AndroidX Paging Library know when to load more pages?

There's this question, which is identically worded.
But the poster's problem was solved without actually answering the question posted - because the problem was due to something unrelated to how the Paging Library knows to load more pages.
I'm wondering if the subscription/collection (Rx/Flow) mechanism used to consume the Paging datasource is how the Paging datasource knows when to load more pages. So maybe subscription/collection pulls pages. (But even so, how would pages after the initial load be triggered?)
And the datasource can use the pull events as an opportunity to check if it needs to load more pages.
I'm just speculating here because I don't know how this all works internally.
I'm hoping someone will either verify or dismiss my speculation and provide greater detail about the mechanism involved. In particular, when the Pages are exposed as a Flow or (Rx) Flowable.
The presenter API's .get(index) call is what triggers item access to the rest of Paging. Generally you need to call this function in order to bind items to a RecyclerView.Adapter, but leanback and compose work similarly.
On item access, an internal object called ViewportHint is sent which contains presenter-state including how many items post-transformation you can see and your current position in the list, which can be handled asyncly from rest of UI.
Paging then essentially triggers loads based on PagingConfig.prefetchDistance, but has a lot of logic handling race conditions, access conflation, cancellations, tracking load state, error handling, etc. which all can affect what ends up getting loaded in the end.
If you want to access but avoid triggering loads, there are .peek() and .snapshot() APIs that let you inspect presenter state without fetching.
There's a LOT of code in the Paging 3 library, and I desperately wanted to avoid having to look at it too closely.
But I did some superficial exploration and found a likely candidate to explain how loads of missing pages are triggered by the user scrolling.
Loads aren't, as I speculated in my question, triggered by the activity of the subscription/collection (Rx/Flow) of pages.
The adapter's attempt to get an item to display triggers a call to
PagingDataDiffer<T : Any>.get(#IntRange(from = 0) index: Int): T?.
Its Javadoc:
Returns the presented item at the specified position, notifying Paging of the item access to trigger any loads necessary to fulfill prefetchDistance.
Params:
index - Index of the presented item to return, including placeholders.
Returns:
The presented item at position index, null if it is a placeholder.
Of particular interest:
... notifying Paging of the item access to trigger any loads necessary to fulfill prefetchDistance.
So, there we have it.
If others disagree or wish to elaborate on the mechanism, please do so.

Using Android PagedList for Charts and Lists

I am using Android API Level 28 with android components.
I have an app that displays our data visually in a chart. I'd like to background load the data and begin charting it as it comes in. I also would like you to be able to view the data as a list.
Ideally, the data for the chart and the list would be the same object instances. After all, the network call to retrieve the data is expensive. Plus, depending on the filters selected, there could be a lot of data to be charted.
I cannot use a server-based chart building API; I must draw graphics from the data on the device.
I am able to download the data in the background and build the chart. I looked into PagedList et al for the list view and that works terrifically.
What I cannot figure out is how to share the data. In other words, if you view the chart first then the list, the list should not trigger a download again since the data is already there. Likewise if the user scrolls through the list then looks at the chart.
This is especially helpful for tablets where one side displays the list and the other is the chart (eg, tapping on an item in the list highlights the data point in the chart).
Any pointers would be greatly appreciated.
What I tend to do is to build a class that handles the downloading of the data, then hand it out to whoever wants it
like
class BananaProvider {
private var _bananaData: List<String>? = null
val bananas: List<String>
get() {
_bananaData?.let{ return _bananaData!!}
_bananaData = getBananaData()
return _bananaData!!
}
fun getBananaData(){
(...)
}
}
and perhaps have a function for refilling your babana's with fresh data if you want.
Then, just drop an instance of the BananaProvider in every fragment/function/whatever that needs it.
Alternatively, if you want to share the data between activities, you could serialize it and send it in an intent, or drop it in an SQLite database if you want to save it locally for later use.
What I wound up doing was:
Created a DataSource with the functions to fetch the data from the server. I'm using a PositionalDataSource.
Created a DataSourceFactory to deliver my DataSource
Created a LiveData using LivePagedListBuilder with my factory and a PagedList.Config (to bring in 100 records at a time, which seems optimal for my needs)
It's the LiveData that I share between the RecyclerView and the chart.
The object which controls the download for the chart keeps track of the current download position and size. If the chart winds up creating the shared LiveData, this will trigger the initial download and populate the underlying PagedList that LiveData is observing.
Each time data comes down, I update the download position and size and draw points for the data just received. If the data isn't complete, I found that I have to wait a few milliseconds before triggering the next download using the PagedList's loadAround function.
If the RecyclerView is the thing which is opened first by the user, its PagedListAdapter automatically brings the data down through the same DataSource and it fills the shared LiveData as the user scrolls.
When I switch back to the Chart, since the LiveData is already populated (all or partially), it can draw the points. If the data isn't complete yet, the PagedList will attempt to load the rest, drawing the points of the data coming down.
It's a pretty neat method. The key was keeping the LiveData around and reusing it and figuring out how to "manually" populate it when not using RecyclerView.

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

How to properly use LiveData with RecycleView

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?

Categories

Resources