Create single onComplete for dynamic list of Completables - android

I'm using this library for wrapping Firebase transactions with RxJava. I'm new to RxJava, so this is mainly a question regarding how to use it.
Scenario: There is a many-to-many relationship between Persons and Labels. A Person can have multiple Labels, and a Label can be given to many Persons. When a Person is created, I must:
add them to the list of Persons
update each Label given to them to allow for querying all Persons that belong to a specific label
I have a list of Labels I want to write to my Firebase database.
List<Label> labels; // Let's assume it's been instantiated and added to
I want to write each of these to the DB:
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference peopleRef = database.getReference().child("people");
DatabaseReference labelsRef = database.getReference().child("labels");
int newPersonId = peopleRef.push().getKey();
I can do this easily if I don't care about whether the calls are successful.
// Let's assume I already saved the Person to the DB
for (Label label : labels){
// For each label, index the Person saved (Looks like 'personId: true')
labelsRef.child(label).child(newPersonId).setValue(true);
}
But what if I do care about the result? If I want to react to all Labels being updated (like navigate away from the current Activity), I need to know if they've all been updated successfully.
RxFirebase is implemented such that setting a value in the DB returns a Completable. I essentially want to zip together n number of Completables and do something only when they succeed or fail.
So far, I can do this if I only want to update one Label, but I want to update n Labels.
The following code snippet chains 2 Completables together, but only saves 1 Label
RxFirebaseDatabase.setValue(peopleRef.child(newPersonId), person) // Save the Person
.andThen(RxFirebaseDatabase.setValue(labelsRef.child(label).child(newPersonId), true)) // I can index 1 Label, and this returns a Completable
How would I do this? If you know Firebase well enough, is this even the right way to be saving a List of items?

If I understood your main question correctly, you have a collection of Completable and you need to subscribe to them as one.
The way to solve this is using the Completable.concat or Completable.merge operators.
Completable.concat: Returns a Completable which completes only when all sources complete, one after another.
Completable.merge: Returns a Completable instance that subscribes to all sources at once and completes only when all source Completables complete or one of them emits an error.
Example:
List<Completable> tasks; // initialized elsewhere
Completable
.concat(tasks)
.subscribe(
() -> Log.d(TAG, "All successful"),
throwable -> Log.w(TAG, "One or more failed"))
About your second question, I don't know Firebase well enough.
Update: to obtain the List<Completable> you can do something similar to this:
List<Completable> tasks = new ArrayList<>();
for ( ... ) {
tasks.add(RxFirebaseDatabase.setValue(peopleRef.child(newPersonId), person));
}
Completable.concat(tasks).etc

Related

Paging 3 - Use Network data as primary source and local data as addition

I want to load network data first and combine it with data from my local room db.
So in that way, I would always present the newest state of the backend to my users (since its always requesting the network first) and update my ui when there are some local db updates.
A use-case example:
1. A UserActivity shows all created posts of a given user (through paging3).
Data has been fetched from network.
2. User starts a PostActivity where he/she likes the post (post was also visible in step 1 (UserActivity)).
This action will trigger a rest call where the result (updated post) will be stored to the local db.
3. User comes back to the UserActivity and sees that the post is marked liked now.
The UI update was done automatically, since the local Db changed in step 2.
Here are some of those things I've tried
Using Rooms PagingSource implementation (LimitOffsetPagingSource)
Initially I thought the Rooms PagingSource + RemoteMediator is exactly what I needed (see this codelab example for implemantation).
The problem with that was though, that after the local database was initially filled with entities (through network), it will be used as the start source for further inital fetches in pagings.That means that changes in my backend would only be noticed by the UI when the user scrolls to the end of the list. Because only then, the LimitOffsetPagingSource will request the RemoteMediator again for further entities, since it is out of items.
A workaround (like used in the codelab) was to delete all entities before paging again.
// clear all tables in the database
if (loadType == LoadType.REFRESH) {
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
}
But this would break all my other views which also depended on these entity models.
Example:
1. TopPostsActivity shows top posts via paging
2. Clicking on a user which starts the UserActivity (from the first example)
3. UserActivity starts paging. This will initially delete all entities, so that we can fetch all new elements first.
User doesn't scroll much, so that not all entities are stored again.
4. User goes back to TopPostsActivity, where it will see an empty list. Reason for that is that the UserActivity deleted all depending posts before.
Only an additional refresh call can show the desired posts again.
Merging local and remote PagingData manually
In this try, I used a custom PagingSource for fetching from network in my Pager (just the basics like here Defining a PagingSource).
Since Room can also provide flows which notifies when there are some changes in the dataset, I transformed the data to a PagingData and submitted it to my PagingAdapter.
//Room DAO
#Query("SELECT * FROM post WHERE author_id = :authorId ORDER BY created_at DESC")
fun findAllByAuthor(authorId: String): Flow<PostWithAuthor>
The merge code
...
val localDbFlow = viewModel.repo.dao.findAllByAuthor(viewModel.author.id)
.map { PagingData.from(it) }
val networkFlow = viewModel.pager.flow
val mergedFlow = merge(localDbFlow, networkFlow)
flowAndCollectLatest(mergedFlow, Lifecycle.State.CREATED) {
adapter.submitData(it)
}
This also kinda worked, but the problem was that I am not getting any LoadState changes anymore due to the function PagingData.from(list).
/**
* Create a [PagingData] that immediately displays a static list of items when submitted to
* [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter].
*
* #param data Static list of [T] to display.
*/
#JvmStatic // Convenience for Java developers.
public fun <T : Any> from(data: List<T>): PagingData<T> = PagingData(
flow = flowOf(
PageEvent.Insert.Refresh(
pages = listOf(TransformablePage(originalPageOffset = 0, data = data)),
placeholdersBefore = 0,
placeholdersAfter = 0,
sourceLoadStates = LoadStates(
refresh = LoadState.NotLoading.Incomplete,
prepend = LoadState.NotLoading.Complete,
append = LoadState.NotLoading.Complete
)
)
),
receiver = NOOP_RECEIVER
)
So I can't finish any refresh ui actions like with SwipeRefreshLayout, since I'm not getting anything back through the LoadStateListener.
Can anyone please help me with this problem? Is there anything I'm overseeing? This issue really starts depressing me and steals the fun I have to code :'(
I can't create a replacement for the automatically generated LimitOffsetPagingSource (since It really seems critical for one without the required qualification).

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
}

Room with LiveData: Many to Many mapping in adapter

Lets take the following example:
A many to many mapping exists for PRODUCTS and ORDERS. So a product can be on multiple orders and an order can have multiple products. In Room I have an entity which has both the product id and order id as foreign keys so I can save the relations. It's now very easy to get all the orders for a specific product and also all the products for a specific order.
Now here comes the trouble. As far as I know there is no way to get the order object with all of it's products in 1 query/entity. This can be read in further detail in this post. In most places I can bypass this by just running two queries. The first to get the order I'm interested in, and the second to get the products based on the Id of the order.
Now I want to display the combination of an order with its products in an adapter. For that I need to combine all my orders with their products. I'm clueless on how to solve this with LiveData.
The best solution in my opinion would be to create one query that fetches the OrderWithProducts directly from the database. This post suggests it should be possible, but I've not managed to get this to work. Also the most crucial part in that example is missing: the OrderItem class.
If that solution is not possible there must be some way to get the LiveData OrderWithProducts list with 2 queries and somehow combine them.
EDIT
After the suggestions of #Demigod now I have the following in my ViewModel:
// MediatorLiveData can observe other LiveData objects and react on their emissions.
var liveGroupWithLights = MutableLiveData<List<GroupWithLights>>()
fun createOrdersWithProducts() {
appExecutors.diskIO().execute {
val ordersWithProducts = mutableListOf<OrderWithProducts>()
val orders = orderRepository.getGroupsSync()
for (order in orders) {
val products = productRepository.getProductsSync(order.id)
val orderWithProducts = OrderWithProducts(order, products)
ordersWithProducts.add(orderWithProducts)
}
liveGroupWithLights.postValue(ordersWithProducts)
}
}
The function inside my fragment to submit data to the adapter:
private fun initRecyclerView() {
orderListViewModel.getOrdersWithProducts().observe(this, Observer { result ->
adapter.submitList(result)
})
}
So now I'm able to have a OrderWithProduct object as the item for my adapter. This is great, I can use products for each order in my adapter. Now I'm having trouble to update these items whenever the values in the database changes. Any ideas for this part?
Edit2: the invalidationtracker
db.invalidationTracker.addObserver(object : InvalidationTracker.Observer("orders", "products", "order_product_join") {
override fun onInvalidated(tables: MutableSet<String>) {
createOrdersWithProducts()
}
})
The problem I have now is that the validation tracker gets notified a lot for a single change.
As far as I know, it's not possible currently with a single query.
To solve this, you will need to run several queries here. At first - obtain a list of orders with a single query, and after that obtain a list of products per each order. To achieve this, I can think of several options:
Make your own OrdersWithProductsProvider, which will return this combined entities (Order with List<Porduct>), and it will subscribe for the changes to database to emit new objects using LiveData on every orders or products table change.
You can use a MediatorLiveData to fill the list of Orders with their Products, but I don't think this is a best approach since you will need to run query in a background thread, maybe use of Rx is more convenient here.
Personally, I would use a first option, since probably I want to obtain up-to-date list of orders with their products, which means that the update should trigger on change of three tables (products, orders, products_to_orders), which can be done via Room.InvalidationTracker. Inside that provider I would use Rx (which can work with LiveData via LiveDataReactiveStreams).
Addition on how to achieve that:
How to achieve that isn't really matters, the only thing - run this whole query in the background thread post it to LiveData. You can use Executor, Rx, or a simple Thread. So it will look something like:
private val database : Database // Get the DB
private val executor = Executors.newSingleThreadExecutor()
private val liveData = MutableLiveData<List<OrderWithProducts>>()
fun observeOrdersWithProducts():LiveData<List<OrderWithProducts>> {
return liveData
}
private fun updateOrdersWithProducts() {
executor.post {
val ordersWithProducts = mutableListOf<OrderWithProducts>()
val orders = db.orders()
for (order : orders) {
val products = database.productsForOrder(order)
val orderWithProducts = OrderWithProducts(order, products)
ordersWithProducts.add(orderWithProducts)
}
liveData.post(ordersWithProducts)
}
}
Take it as not complete working code, rather an example of implementation.
Call updateOrdersWithProducts on initialization/first call and every time InvalidationTracker will notify about the db change.

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

Rxjava Filtering the duplicate item in List

I am using RxJava and Retrofit in My App.Here's the setup of the app.When the app is launched the app make two request one to the database and other to the Network Api (using Retrofit) and both request return a Observable<List<Article>>. So what I did is basically merged the two Observable. Now the problem is sometimes the network return Articles that are already present in the Database. So how do I filter out the duplicate item from the List. Here's my Code.
return Observable.merge(dataSource.getArticles(source), remoteSource.getArticles(source))
.distinct();
So I tried distinct operator but it's not filtering the Articles out.Here's the output looks like form db.
Article1
Article2
Article3
Article4
Output from Network
Article7
Articke8
Article3
Article4
What I want is a distinct list of Article
Assuming your Article has proper equals implementation,
you could collect them into a set:
dataSource.getArticles(source)
.mergeWith(remoteSource.getArticles(source))
.collect(HashSet::new, (set, list) -> set.addAll(list))
or you could unroll each list and apply distinct followed by toList:
dataSource.getArticles(source)
.mergeWith(remoteSource.getArticles(source))
.flatMapIterable(v -> v)
.distinct()
.toList()
That's because they are returning different lists. So the distinct method recognize them as different items
If you want to emit first the database items and then add the server ones... This may be a bit more complex but not too much ;)
Observable<List<Article>> databaseArticles = ...
Observable<List<Article>> serverArticles = ...
Observable<List<Article>> allArticles =
Observable.combineLatest(
databaseArticles,
serverArticles
.startWith(emptyList()), // so it doesn't have to wait until server response
(dbItems, sItems) => {
// Combine both lists without duplicates
// e.g.
Set<Article> all = new HashSet<>();
Collections.addAll(all, dbItems);
Collections.addAll(all, sItems);
return new ArrayList<>(all);
});

Categories

Resources