Retrieving a HashMap LiveData via Room Library - android

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.

Related

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

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?

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 handle Android LiveData changes from two different ways (Room, user)?

I have an Android app with a Room database which consumes REST API.
Room is acting as single source of truth, i.e. I am updating UI when API result is saved in the Room.
In one of my screens, I need to show a filtered list (with the latest updates from API), for example, List of movies filtered by author.
When the user changes author filter, the list needs to be updated, but also, the list needs to be updated when movies change in the backend as a result of an API call (stored in the db).
Second I can achieve with LiveData> object that is created from Room call, it will dispatch changes from Room db.
But, how do I incorporate changes activated from user (by switching filter) over same source (filtered list of movies)?
For anyone else, it's actually quite simple with MediatorLiveData.
val selectedItem = MediatorLiveData<Voyage>()
var voyages: LiveData<Resource<List<Voyage>>>
var voyageFilter = MutableLiveData<VoyageFilter>()
selectedItem.addSource(voyageFilter) { filter ->
//do something
}
selectedItem.addSource(voyages) { listResource ->
//do something
}

How to do a single row query with Android Room

How do I make a single row query with Android Room with RxJava? I am able to query for List of items, no issues. Here, I want to find if a specific row exists. According to the docs, looks like I can return Single and check for EmptyResultSetException exception if no row exists.
I can have something like:
#Query("SELECT * FROM Users WHERE userId = :id LIMIT 1")
Single<User> findByUserId(String userId);
How do I use this call? Looks like there is some onError / onSuccess but cannot find those methods on Single<>.
usersDao.findByUserId("xxx").???
Any working example will be great!
According to the docs, looks like I can return Single and check for EmptyResultSetException exception if no row exists.
Or, just return User, if you are handling your background threading by some other means.
#Query("SELECT * FROM Users WHERE userId = :id")
User findByUserId(String id);
How do I use this call?
usersDao.findByUserId("xxx")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(user -> { ... }, error -> { ... });
Here, I show subscribe() taking two lambda expressions, for the User and the error. You could use two Consumer objects instead. I also assume that you have rxandroid as a dependency, for AndroidSchedulers.mainThread(), and that you want the User delivered to you on that thread.
IOW, you use this the same way as you use any other Single from RxJava. The details will vary based on your needs.
According to the Google docs: For single result queries, the return type can be any data object (also known as POJOs). For queries that return multiple values, you can use java.util.List or Array.
Google docs

Android Room requery on db change

I'm writing an application using newest Room Persistance Library.
The app sipmply shows a list of items and updates this list as data changes.
When new item is inserted into a table, or updated, I expect the list to update automaticlally.
I tried vanilla LiveData and Flowable so far. Both are claimed to support this feature, as it is stated in documentation and on this blog:
https://medium.com/google-developers/room-rxjava-acb0cd4f3757
Here's the ViewModel snippet in Kotlin:
messagesFlowable = db.messagesDao().all()
messagesFlowable
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Log.d(TAG, "Received 1 list of %s items", it.size)
messages.value = it
}
Somewhere else, the db is modified like this:
mDb.messagesDao().add(Message("Some data"))
The updates are not pushed to observers. I guess I'm missing something, but what?
Update: This problem is solved and the answer is below.
I'll answer my own question, as the solution is not documented.
It looks like you need to have the same instance of database object.
In my case, my Dagger2 was misconfigured to inject new instances of DB each time, so my Repository and ViewModel ended up with 2 separate instances.
Once I use single database instance shared among all interested parties, all updates are distributed correctly.

Categories

Resources