How to add another data to items loaded by Paging Library? - android

I use Android Architecture Components to build my app. There is Paging Library to load items with Room generated DataSource. Also there is BoundaryCallback to get new data from server and store it in the database. It works fine, all is reactive, changes in the database come into PagedList.
But now I need to these items get some additional data, some calculations before they come into PagesList and RecyclerView. These calculations is not so fast to executing them on main thread in RecyclerView ViewHolder (actually I need to get additional data from the database or even from the server). So I supposed that I need to write my custom DataSource and make calculations there and then pass these processed items to PagedList.
I created my ItemKeyedDataSource (I'm not sure this is correct, because I load data from database, but this data source type is designed for network, but I don't think this is critical), and make queries in Dao that return List of items. After I got a "page", I make calculations to items and then pass it to callback. It works, PagedList gets processed items.
But unfortunately there is no reactivity with this approach. No changes in database come to my PagedList. I tried to return LiveData<List> from Dao and add observeForever() listener in DataSource but it fails since you can't run it on background thread.
I watched Room generated DataSource.Factory and LimitOffsetDataSource but it doesn't look good to me since you need to pass table names to observe changes and other unclear things.
I suppose that I need to use invalidate(), but I don't because I have no idea where it should be.
There is 3 main questions:
Is it right to process items in DataSource before they come to RecyclerView or there is a better place?
Should I use PositionalDataSource instead of ItemKeyedDataSource?
How can I add Room reactivity to custom DataSource?

It seems that I've found a mistake in my DataSource.Factory. Instead of creating DataSource object in create() method I just returned object which was passed to that factory (I saw it in one popular article on Medium). And because of that I couldn't invalidate my DataSource. But now I create DataSource in that method and invalidation works.
The only problem is to understand where and when to invalidate. For now I've found some workaround: make a query in Dao that returns LiveData of last item, and then observe it in my Activity to understand that data was modified and call invalidate(). But I'm not sure this is a good solution. Maybe you know a better one.

You may add invalidationTracker in your DataSource:
dbRoom.getInvalidationTracker().addObserver(
object : InvalidationTracker.Observer("your_table") {
override fun onInvalidated(#NonNull tables: Set<String>) {
invalidate()
}
})

Related

Paging 3 RemoteMediator and SavedStateHandle

I'm using the RemoteMediator in an app to load page keyed data. Everything works fine, except when after process death, the data is refreshed.
My current implementation is :
val results = savedStateHandle.get<String>("query").flatMapLatest { query ->
repository.getPager(
query = query,
)
}.cachedIn(viewModelScope)
I do know about the initialize() function of RemoteMediator, but how do I tie it in with process death?
As you found out, .cachedIn just operates in memory, so it won't survive process death. You cannot rely on Paging's internal cache of items in memory for this, you need to cache the loaded items on disk.
I would recommend using something like Room or some dedicated persistence layer that is actually built to handle large lists of arbitrary data classes.
I would not recommend to try to serialize and stash the entire list of data into SavedState as this could become prohibitively expensive quite quickly.
For your other point on RemoteMediator - it is just a "dumb" callback which has no influence on what Paging actually loads or displays. It's simply a way for your to write custom logic which is triggered during edge-case conditions in Paging. You probably only want this if you are already using a layered approach and trying to skip remote REFRESH. If that is your case, the RemoteMediator.intiailize function is guaranteed to complete before Paging starts loading, which means you can check whether you are coming from SavedState and there is already cached data, and if so, you can skip remote REFRESH by returning InitializeAction.SKIP_INITIAL_REFRESH.

Call notifiyDataSetChanged() from android viewmodel

I'm making a reactive app with a lot of data using Android MVVM. There are a lot of cases where I push data to firestore documents in my app, but i also want this change in data to be reflected locally in my app. So I also add the data to the local copy (data class) of the firestore document. I do this in my viewmodel.
The problem here is that I won't see change until I restart the app. So I need a way to call adapter.notifyDataSetChanged() from my viewmodel so the recyclerviews are updated immediately. Whats the best way to do this? Please help.
As you change and update data frequently, adapter.notifyDataSetChanged isn't helpful in this case because even if you update one item it will update the whole list even when the rest not touched!
So A better way to make your adapter
1- extend ListAdapter<data_model, view_holder>(diff_callback).
this Uses AsyncListDiffer under the hood to calculate and update the changed items only
2-whenever you need to update your data in viewmodel call:
adapter.submitList()
For more information on how to do that:
check doc: https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter
Yours ViewModel should't call adapter.notifyDataSetChanged() directly for two reasons:
Adapter is Android dependency. And if you want to write unit tests to yours view model you should avoid use android framework dependencies
I think adapter.notifyDataSetChanged() is UI logic implementation detail. And will be better if we move this logic to our view layer (fragment/activity/custom views)
You could solve this problem by encapsulate notifyDataSetChanged logic in yours adapter like this:
// Observe live data changes and set it to yours adapter
viewModel.state().observe(this, Observer {
newData: List<....> ->
adapter.setData(newData)
})
// Encapsulate notifyDataSetChanged at yours adapter:
class YoursAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun setData(data: List<.....>) {
......
notifyDataSetChanged()
}
}
In this case it's very easy to change adapter implementation from notifyDataSetChanged() to more precise updates like notifyItemInserted() later for example.
But I'm also recommend do not use notifyDataSetChanged() function and prefer DiffUtil. Or use libraries that can help with recycler view boilerplate and already have DiffUtil support under the hood. For example I like to use Groupie in my projects

How to send search suggestions dynamically to data source and get updated pagelist in Android jetpack paging library?

How to send search suggestions dynamically to data source's retrofit params and get updated pagedlist in Android jetpack paging library?
Here's my web function in retrofit web service that brings data.
#GET(version + "/get-bills")
Call<ApiResponse<BillsModel>> getPartnerBillsSorted(#Query("page")int page, #Query("type")int type,#Query("search")String search );
This api is being called in data source and it gives me listing of all data, which i am listing in reyclerview using pagedlist. But i am quite confused and dont know how do i give call to this api in Data source on runtime, and then get updated pagedlist based on what i am seaching in autocompleteTextview.
what should i do? should i make new instance of datasource and data source factory and pagelist everytime i tap something in autocomplete or how do i dynamically change same datasource call and get updated pagelist?
You do not need to to change anything in your web service. You may make a constant class(getter setter) etc and change your data source class like this.
compositeDisposable.add(
networkService.getPartnerBillsSorted(Costant.PAGE,
,Costant.TYPE
,Costant.SEARCH
.subscribe()
and then call this in your search complete listener in activity/fragment:
Constants.setSearch("search result");
viewModel.searchResultList.value.dataSource.invalidate();
create a field in your Datasource class(add getter, setters), pass this field to retrofit call. later if you want make another call just set the required search query to datasource instance and call .invalidate() after setting the query.
refer this article
https://android.jlelse.eu/android-paging-library-make-your-lists-as-efficient-as-possible-literally-in-just-an-hour-5abf797585bd

Android - Room Persistence Library - Access data both synchronously & through observer based on need

Problem:
I am using Room Persistence Library and so far everything is working fine except that there is a data from select query which I need synchronously as I am calling it from a Periodic Job (Work Manager's Worker). I have defined the return type to be LiveData as I am also accessing it for display purposes in UI and so observers are great for that but now I also need the same data in Job.
Code Snippet
#Query("SELECT * from readings ORDER BY date, time ASC")
LiveData<List<Reading>> getAllReadings();
Tried
I have tried the getValue() method in LiveData but it returns null as the data is not loaded in LiveData while making the query.
readingDao().getAllReadings().getValue() // returns null
Possible Solution
There is only one solution that I can think of which is to duplicate the getAllReadings query with a different name and return type (without LiveData) but I don't think this is a clean approach as it increases duplication of code just to get a synchronous return type.
Please let me know if there is any other solution or perhaps some way to synchronously access data from LiveData variable.
You can allow main thread query when you initialize Room DB, but it's clearly not desirable. This will give you the synchronous behavior but will block user interface. Is there a specific reason you want this to be synchronous?
The reason why getValue() is returning null is because Room is querying data asynchronously. You can attach an observer or a callback function to get result when the query is finished. You can display the result to the UI or chain another call for sequential operation etc from there.
I use RxJava to wrap my query request for asynchronous query but I you can also use AsyncTask.

Paging Library with custom DataSource not updating row on Room update

I have been implementing the new Paging Library with a RecyclerView with an app built on top of the Architecture Components.
The data to fill the list is obtained from the Room database. In fact, it is fetched from the network, stored on the local database and provided to the list.
In order to provide the necessary data to build the list, I have implemented my own custom PageKeyedDataSource. Everything works as expected except for one little detail. Once the list is displayed, if any change occurs to the data of a list's row element, it is not automatically updated. So, if for example my list is showing a list of items which have a field name, and suddenly, this field is updated in the local Room database for a certain row item, the list does not update the row UI automatically.
This behaviour only happens when using a custom DataSource unlike when the DataSource is obtained automatically from the DAO, by returning a DataSource Factory directly. However, I need to implement a custom DataSource.
I know it could be updated by calling the invalidate() method on the DataSource to rebuild the updated list. However, if the app is showing 2 lists at a time (half screen each for example), and this item appears in both lists, it would be needed to call invalidate() for both lists separately.
I have thought with a solution in which, instead of using an instance of the item's class to fill each ViewHolder, it uses a LiveData wrapped version of it, to make each row observe for changes on its own item and update that row UI when necessary. Nevertheless, I see some downsides on this approach:
A LifeCycleOwner (such as the Fragment containing the RecyclerView for example) must be passed to the PagedListAdapter and then forward it to the ViewHolder in order to observe the LiveData wrapped item.
A new observer will be registered for each list's new row, so I do not know at all if it has an excessive computational and memory cost, considering it would be done for every list in the app, which has a lot of lists in it.
As the LifeCycleOwner observing the LiveData wrapped item would be, for example, the Fragment containing the RecyclerView, instead of the ViewHolder itself, the observer will be notified every time a change on that item occurs, even if the row containing that item is not even visible at that moment because the list has been scrolled, which seems to me like a waste of resources that could increase the computational cost unnecessarily.
I do not know at all if, even considering those downsides, it could seem like a decent approach or, maybe, if any of you know any other cleaner and better way to manage it.
Thank you in advance.
Quite some time since last checked this question, but for anyone interested, here is the cause of my issue + a library I made to observe LiveData properly from a ViewHolder (to avoid having to use the workaround explained in the question).
My specific issue was due to a bad use of Kotlin's Data Classes. When using them, it is important to note that (as explained in the docs), the toString(), equals(), hashCode() and copy() will only take into account all those properties declared in the class' constructor, ignoring those declared in the class' body. A simple example:
data class MyClass1(val prop: Int, val name: String) {}
data class MyClass2(val prop: Int) {
var name: String = ""
}
fun main() {
val a = MyClass1(1, "a")
val b = MyClass1(1, "b")
println(a == b) //False :) -> a.name != b.name
val c = MyClass2(2)
c.name = "c"
val d = MyClass2(2)
d.name = "d"
println(c == d) //True!! :O -> But c.name != d.name
}
This is specially important when implementing the PagedListAdapter's DiffCallback, as if we are in a example's MyClass2 like scenario, no matter how many times we update the name field in our Room database, as the DiffCallback's areContentsTheSame() method is probably always going to return true, making the list never update on that change.
If the reason explained above is not the reason of your issue, or you just want to be able to observe LiveData instances properly from a ViewHolder, I developed a small library which provides a Lifecycle to any ViewHolder, making it able to observe LiveData instances the proper way (instead of having to use the workaround explained in the question).
https://github.com/Sarquella/LifecycleCells

Categories

Resources