Android paging2 library: Network(PageKeyedDataSource) + Database idiomatic/expected way to implement - android

I'm not considering Paging3 since it is still in alpha as of now, So, any suggestions on implementing this using paging 3 are definitely welcome but won't be much useful in context.
From the paging 2 (db+network)sample:
RedditPostDao.postsBySubreddit() returns DataSource.Factory
fun postsBySubreddit(subreddit: String): DataSource.Factory<Int, RedditPost>
Which is used with SubRedditBoundaryCallback to create LiveData of PagedList of RedditPost
SubRedditBoundaryCallback has onItemAtEndLoaded(itemAtEnd: RedditPost) {…}
Which gets called when DB can no longer provide further items.
This works well in this case as the reddit API used is ItemKeyed like.
My question is:
How do I use the paging 2 with Network + Database if my web API is PageKeyed. I would need the nextPageLink to make the network call from the BoundaryCallback's onItemAtEndLoaded() but this method returns the last item and doesn't have any information on next page link.
Here is how I did it to make it work with page keys. The following implementation works but I wanted to see if there is more idiomatic/expected way of doing this.
I created another Room table for Entity:
RedditPostPageLink(redditPostName: String, nextPageKey: String)
When I receive the data from the network API, I pick up the last item in the response and make and entry in the RedditPostPageLink(name, nextPageKey).
In the BoundaryCallback. onItemAtEndLoaded(), when I get and itemAtEnd, I get the corresponding nextPageKey that I had earlier stored and make a web API call based on that.
Given that page sizes are pretty much fixed, is there a better version to achieve this with paging 2?

Related

Android architecture LiveData and Repositories

I am converting my application to room database and try to follow the google architecture best practices based on "Room with a View".
I am having trouble to understand the repository in terms of clean architecture.
The Words database example contains only one table and one view using it, making it a simple HelloWorld example. But lets start with that.
There is a view which displays a list of words. Thus all words need to be read from the database and displayed.
So we have a MainActivity and a Database to connect.
Entity Word
WordDao to access DB
WordViewModel: To separate the activity lifecycle from the data lifecycle a ViewModel is used.
WordRepository: Since the data maybe kept in a database or the cloud or whatever the repository is introduced to handle decision, where data comes from.
Activity with the View
It would be nice if the view is updated when the data changes, so LiveData is used.
This in turn means, the repository is providing the LiveData for the full table:
// LiveData gives us updated words when they change.
val allWords: LiveData<List<Word>>
This is all fine for a single view.
Now to my questions on expanding this concept.
Let us assume, the word table has two columns "word" and "last_updated" as time string.
For easier comparison the time string needs to be converted to milliseconds, so I have a function.
Question: Where to put the fun queryMaxServerDateMS() to get the max(last_updated)?
/**
* #return Highest server date in table in milliseconds or 1 on empty/error.
*/
fun queryMaxServerDateMS(): Long {
val maxDateTime = wordDao.queryMaxServerDate()
var timeMS: Long = 0
if (maxDateTime != null) {
timeMS = parseDateToMillisOrZero_UTC(maxDateTime)
}
return if (timeMS <= 0) 1 else timeMS
}
For me it would be natural to put this into the WordRepository.
Second requirement: Background job to update the word list in the database.
Suppose I now want a Background Job scheduled on a regular basis which checks the server, if new entries were made and downloads them to the database. The app may not be open.
This question just relays to the question of the above queryMaxServerDateMS.
The job will basically check first, if a new entry was made by asking the server if an entry exists which is newer then the max known entry.
So I would need to get a new class WordRepository, do my query, get max last_update and ask the server.
BUT: I do not need the LiveData in the background job and when val repositoy = WordRepository the full table is read, which is needless and time-, memory and batteryconsuming.
I also can think of a number of different fragments that would require some data of the word table, but never the full data, think of a product detail screen which lists one product.
So I can move it out to another Repository or DbHelper however you want to call it.
But in the end I wonder, if I use LiveData, which requires the View, ViewModel and Repository to be closely coupled together:
Question: Do I need a repository for every activity/fragment instead of having a repository for every table which would be much more logical?
Yes, with your current architecture you should put it in the Repository.
No, you don't need a repository for every activity/fragment. Preferably, 1 repository should be created for 1 entity. You can have a UseCase for every ViewModel.
In Clean architecture there's a concept of UseCase / Interactor, that can contain business logic, and in Android it can act as an additional layer between ViewModel and Repository, you can create some UseCase class for your function queryMaxServerDateMS(), put it there and call it from any ViewModel you need.
Also you can get your LiveData value synchronously, by calling getValue().
You do not need repository for each activity or fragment. To answer your question about getting max server time - when you load words from db you pretty much have access to entire table. That means you can either do that computation yourself to decide which is the latest word that's added or you can delegate that work to room by adding another query in dao and access it in your repo. I'd prefer latter just for the simplicity of it.
To answer your question about using repo across different activities or fragment - room caches your computations so that they are available for use across different users of your repo (and eventually dao). This means if you have already computed the max server time in one activity and used it there, other lifecycle owners can use that computed result as far as the table has not been altered (there might be other conditions as well)
To summarize you're right about having repository for tables as opposed to activities or fragments

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

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()
}
})

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

Retrieving a HashMap LiveData via Room Library

I have a social networking app which displays a list of users, and am looking to have an efficient way of being able to retrieve an object from my LiveData using its primary key.
Example: Retrieve a set of User POJOs from within my LiveData<List<User>> given a LIST of userId Integers (ie, users 12, 5, 7, and 1). I need to be able to look up these users by the userId for display in the appropriate order in the UI.
I believe I want something more like LiveData<Map<Integer, User>>, but how could I implement this using the Room database, without breaking the LiveData callbacks from my local DB -> Room -> LiveData -> UI?
PROPOSAL 1:
Change my Room implementation to somehow return a LiveData containing a HashMap of <userId,User>.
Current Room implementation:
#Query("SELECT * FROM users WHERE user_id in :userIds LIMIT 1")
LiveData<List<User>> getUsers(List<Integer> userIds);
Proposed Room implementation (no idea if something like this is possible or what it would even look like):
#Query("SELECT * FROM users WHERE user_id in :userIds LIMIT 1")
LiveData<**HashMap**<Integer,User>> getUsers(List<Integer> userIds);
PROPOSAL 2:
Have a list of many LiveData objects WITHIN a Map:
Map<Integer,LiveData<User>> liveDataUsers;
This might be something to look into, but I'm worried that having potentially hundreds/thousands of LiveData objects within a map is bad design and could also lead to performance issues / too many open LiveData internal callback threads.
PROPOSAL 3:
Something else??? I feel like I am missing something easy here. How are others looking up objects within their LiveData using only their primaryKey?
edit: this is something I'd like to achieve at the Repo / Model level and not at the activity level, as this LiveData will be re-used throughout the app.

Android: Repository-Pattern and Pagination

Recently, I've read about how important it is to have a Single-Source-Of-Truth (SSOT) when designing an app´s back-end (repository, not server-side-back-end). https://developer.android.com/topic/libraries/architecture/guide.html
By developing a news-feed app (using the awesome https://newsapi.org/) I am trying to learn more about app architecture. However, I am unsure of how to implement paging. By the way,.: I am using MVVM for my presentation layer. The View subscribes to the ViewModel´s LiveData. The ViewModel subscribes to RxJava streams.
I came up with the following approach:
interface NewsFeedRepository {
fun getNewsFeed(): Observable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
The ViewModel subscribes to the getNewsFeed() Observable which emits data every time the underlying data in the database (SSOT) changes. However, my question is regarding the loadMore() method.
This method attempts to load more articles from the API and on success insert them into the local database. This causes getNewsFeed() to emit the new feed data.
My questions:
1. Should the repository be responsible for syncing API and local database data? Or should the repository use some "low-level" API that manages syncing network/local data?
2. The method loadMore returns a Completable which might seem weird/misleading. Are there better ways to indicate paging in the repository interface?
3. Paging means storing a current page count and use it when requesting data from the API. Where should the current page count be stored? In the repository? Or in some "lower-level" component that is being used by the repository?
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Flowable<List<Article>>
fun moreArticles(): Completable
}
This repository is used to search articles. The method moreArticles() attempts to load more articles from the API depending on the last call to searchArticles(...). For example: The ViewModel calls repo.searchArticles(q = "bitcoin"). A call to repo.moreArticles() attempts to load more articles that contain the query string "bitcoin" from the API and sync with the local database.
Again my questions:
1. Is it OK to store information about the last request (searchArticles(request) in the repository? I could also think of passing the request parameters in moreArticles() too, e.g. moreArticles(sources: List<NewsSource>? = null, query: String? = null). This way the repository doesn't have to store information about the last request.
2. Again, returning a Completable in moreArticles() might seem weird. Is this OK, or are there better ways?

Categories

Resources