android paging 3 line after adapter.submitData doesn't get executed - android

I am currently learning Paging 3 library. I have an app that has shimmer placeholder for showing while loading the data. When i get the data i want to hide shimmer layout and show recycler view.
lifecycleScope.launch {
quoteViewModel.getQuotes2().collectLatest {
binding.shimmerLayout.visibility = View.GONE // this line gets executed when view created and it causes shimmer layout to hide
binding.homeRecyclerView.visibility = View.VISIBLE
homeAdapter.submitData(it)
Log.d("mytag", "after submit data") // this line isn't executed
}
}
Problem here is the line after the submitData doesn't get executed.I searched and saw the reason was because submitData never returns so it doesn't get called. But i didn't find any example code. Can someone please show me an example code for solving this?
The logic i want to execute after submitData is to hide shimmerLayout and show recyclerView, but because that logic is before submit data, they are executed immediately, therefore shimmerLayout doesn't seem when loading data.

As you mentioned, it is true that the line after submitData() won't be executed since it doesn't return. What is it exactly that you are wanting to achieve using Paging 3 with it here?
I had a similar doubt where my recyclerview was not updating data fetched from Paging 3 because I was using collect operator. Dustin Lam, the author of Paging 3 library answered me here to replace the collect with collectLatest and that had fixed the issue for me.
In your case, you are already using collectLatest. One quick fix can be to put the submitData at the very end of collectLatest since that is what causes the issue.
As mention in the collectLatest definition,
Terminal flow operator that collects the given flow with a provided action. The crucial difference from collect is that when the original flow emits a new value then the action block for the previous value is cancelled.
Note that replacing collect with collectLatest fixed the issue for my case only because initially my StateFlow was emitting an Initial state, but when the data was ready, it emitted a Success state that wasn't collected by the collect operator since the submitData never returns, but collectLatest forces the previous action block to be cancelled and new value to be collected.
I'm unsure of a solution to your problem yet but either edit your question with the exact use case or in case you do not find any alternative to it, you can directly ask Dustin Lam about it like I did :)

I tried with: submitData(lifecycle, it) and it started to stop onto the lines after the Submit.
Here says: Caution: The submitData() method suspends and does not return until either the PagingSource is invalidated or the adapter's refresh method is called. This means that code after the submitData() call might execute much later than you intend.

Related

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.

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
}

Android Work Manager: Can I force prune the Completed Jobs?

I am uploading photos in my app and want to provide the User with the upload progress. The way I was going to do this is have a global tag, UPLOAD_MEDIA_TAG and use that whenever I am creating a OneTimeWorkRequest. That way I can just observe the WorkManager.getInstance().getStatusesByTag(UPLOAD_MEDIA_TAG) LiveData, and whenever a job completes I would show a percent completed. (I.E. Uploading 3/10 photos would show 30%). Then once all 10/10 photos have been uploaded, I would hide the progress bar. This works great as it easily supports leaving the app, coming back, and you will see the Progress Bar as soon as the app launches right where they left off.
The problem is that once all 10/10 jobs are finished and marked as completed, those jobs will stick around until the WorkManger prunes them. So if the user uploads 5 more photos, the LiveData is returning 15 statuses instead of 5, throwing off the percent calculation.
I noticed there is a method OneTimeWorkRequest.keepResultsForAtLeast(duration) but what I would want is something like, keepResultsForAtMost(duration) or a WorkManager.getInstance().forcePrune() method that would cleanup all the completed jobs from the database.
Any help would be great!
After submitting an issue with Google, they are saying there will be a function called, pruneWork in Alpha03.
As of alpha release, you don't have much control over job pruning since it is managed by WorkManager. You can however observe the lifecycle of WorkManager to get job status and take necessary action as follows:
WorkManager.getInstance().getStatusById(myWork.getId())
.observe(lifecycleOwner, workStatus -> {
// Do something with the status
if (workStatus != null && workStatus.getState().isFinished()){
// Stop observing data or do other action
}
});
Note:
Use [WorkManager 1.0.0-alpha03 which has several bugs fixed and new features introduced. One you might be interested is:
Added WorkManager.pruneWork() to remove completed jobs from the internal database

ObjectBox lazyList behaviour

I'm not sure I understand from the documentation how should I use the Lazy List.
What the different between findLazy() and findLazyCached() the function description is exactly the same.
Should I make a find() query first time and just then use findLazy()?
Example of using:
Box<FastCacheData> box = box.boxFor(FastCacheData.class);
LazyList<FastCacheData> build = box.query().build().findLazy();
What the different between findLazy() and findLazyCached() the function description is exactly the same.
They both return a LazyList, which will only load the member objects as they're each accessed. The difference between the two is that the cached version will cache the object so that further accesses won't result in extra loads - the non-cached version will load a fresh object every time.
Should I make a find() query first time and just then use findLazy()
It's a question of when you want the loading to happen. If you want the whole thing loaded when the find() call is made, use the find() call. Else if you want to defer the loading to when you access the data, use the findLazy() call.

Updated RealmResults with time-based query

I'm starting to approach this wonderful world of Realm. I'm very happy of the results I'm getting and now I have one question to submit.
In my android app I've got a Fragment that displays data retrieved from Realm. The query condition is that the time this data refers to is in between the beginning and the end of today.
RealmResults<Appointment> results = realm
.where(MyObject.class)
.between("begin", rangeBegin, rangeEnd)
.between("end", rangeBegin, rangeEnd)
.findAllSorted("begin", Sort.ASCENDING);
This query is executed in the onStart() method helping me to exploit the live-update feature, which indeed works very well.
I've also added listeners for changes in order to optimize UI updates.
Now the question is: how does this live-update behave if the time conditions change? (Imagine I keep the app opened for more than one day without touching it or simply I keep the app opened for minutes around midnight)
From what I've seen it seems to do the same query done the very first time onStart() was executed.
Is there a way to have also live-updating query or should I re-run that query somewhere else outside onStart()?
Thank you in advance
Now the question is: how does this live-update behave if the time conditions change? (Imagine I keep the app opened for more than one day without touching it or simply I keep the app opened for minutes around midnight)
The query is pretty constant after you've set it up, so you'd need to execute a new query with different parameters for rangeStart and rangeEnd, and replace your other results.
This query is executed in the onStart() method helping me to exploit the live-update feature, which indeed works very well. I've also added listeners for changes in order to optimize UI updates.
Personally I'd advise to put the query in onCreateView() instead, and the Realm lifecycle management to onCreateView() and onDestroyView().
Also, you can avoid manually assigning RealmChangeListeners for displaying lists if you use RealmRecyclerViewAdapter (adapters 1.3.0 works with realm 1.2.0).
If you use RealmRecyclerViewAdapter, then just call adapter.updateData(newResults); and it'll update the view as needed.

Categories

Resources