Joining flattened data from Firebase Realtime Database using RxJava - firebase

Joining flattened data is a common use case also described in the documentation. But the documentation shows a simple example which is not real-time, it doesn't react to changes. I'm looking for a more robust implementation. I think RxJava is ideal for this.
Consider following Firebase structure:
{
"messages": {
"group_id_1": {
"message_id_1": {
"text": "Hello",
"author": "uid_1"
}
}
},
"users": {
"uid_1": {
"name": "David"
}
},
"rooms": {
"room_id_1": {
"name": "General",
"members": {
"uid_1": true
}
}
}
}
I see two use-cases here:
Get list of messages in a group with author names
I imagine I would get Observable<Message> and when I subscribe to it, dependencies (users for those messages) will be subscribed as well in some cache. When I'm showing the messages, I can get author's names from the cache.
It's also real-time - if author name changes, the observable emits changed Message.
When I unsubscribe to the observable, also dependencies unsubscribes.
Get a list of room members with their names
I imagine I would get Observable<User> and when I subscribe to it, it will first subscribe to room's members and then to individual users.
It's real-time - if room members change, I get notified about that.
When I unsubscribe to the observable, also dependency unsubscribes.
Do you know about library/solution which could do that?
Or would you use it if I created one?

I was going to pose a variation of this question but seemed like it might be better to build on top of this one...I'll describe what is hopefully at least partially the answer to above question but also then a shortcoming I'm seeing.
Using above data model we might have something like following to create RxJava wrapper around firebase query to get list of member keys for particular room and for getting details for particular member (note use of onCompleted() in subscriber.getMemberInfo...more on that later!).
public Observable<String> getRoomMembers(String roomId) {
return Observable.create(subscriber -> {
databaseReference.child(roomId + "/members").addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot childSnapshot: dataSnapshot.getChildren()) {
String userId = childSnapshot.getKey()
subscriber.onNext(userId);
}
subscriber.onCompleted();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
});
}
public Observable<Member> getMemberInfo(String memberId) {
return Observable.create(subscriber -> {
databaseReference.child(memberId).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Member member = dataSnapshot.getValue(Member.class);
subscriber.onNext(member);
subscriber.onCompleted();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
});
}
What we can then do use something like following to get list of Members for particular room (have added isActive property to Member to show how we also filter results we get).
getRoomMembers(roomId)
.flatMap(memberId -> getMemberInfo(memberId))
.filter(Member::isActive)
.toList()
.subscribe(members -> {
});
So, the above works up to a point. The issue is that I had to call subscriber.onCompleted() in getMemberInfo for the above call to flatMap to work....which then means that any subsequent changes to Member data isn't triggering update in the above subscription. Am relatively new to RxJava and Firebase so might be missing something obvious.

I eventually solved it with these two methods (in Kotlin, Java is similar, just more verbose):
fun <A, B> Observable<List<A>>.mapSubQueries(subQuery: (A) -> Observable<B>): Observable<List<Observable<B>>> {
return this.flatMap {
if (it.isNotEmpty()) {
return#flatMap Observable.from(it).map { subQuery(it) }.toList()
} else {
return#flatMap Observable.just(listOf<Observable<B>>())
}
}
}
#Suppress("UNCHECKED_CAST")
fun <T> Observable<List<Observable<T>>>.joinSubQueries(): Observable<List<T>> {
return this.flatMap {
if (it.isNotEmpty()) {
return#flatMap Observable.combineLatest(it, {
val list = mutableListOf<T>()
it.forEach {
list.add(it as T)
}
list
})
} else {
return#flatMap Observable.just(listOf<T>())
}
}
}
To get users in all messages, I can use it like this:
fun usersInMessages(roomId): Observable<List<User>> {
return DatabaseRead.messages(roomId)
.mapSubQueries { DatabaseRead.user(it.getAuthor()) }
.joinSubQueries()
}
I decided that it's better to keep this code in my codebase and modify it slightly for various use-cases. Making it a library would make it less flexible. The main point is always use Observable.combineLatest(). Many other Rx parameters are useless, because they require onComplete() call and here I deal with infinite Observables.

Related

How to know FirebaseRecyclerAdapter query is zero or not, exist or not

How to know FirebaseRecyclerAdapter query is zero or not, exist or not
I find some instructions on
https://github.com/firebase/FirebaseUI-Android/tree/master/database
it says:
Data and error events
When using the FirebaseRecyclerAdapter you may
want to perform some action every time data changes or when there is
an error. To do this, override the onDataChanged() and onError()
methods of the adapter:
FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(options) {
// ...
#Override
public void onDataChanged() {
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
}
#Override
public void onError(DatabaseError e) {
// Called when there is an error getting data. You may want to update
// your UI to display an error message to the user.
// ...
}
};
When I tried to use as follow:
mAdapter = new FirebaseRecyclerAdapter<Place, PlaceViewHolder>(options)
{
#Override
public void onDataChanged(DataSnapshot dataSnapshot)
{
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
if (dataSnapshot.exists())
{
Log.d(TAG,"data exists");
}
else
{
Log.d(TAG,"No data exists");
}
}
#NonNull
#Override
public PlaceViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i)
{
Error message is:
Method does not override method from its superclass
So how can I solve this problem, Thank you.
I found the answer from another page:
https://www.programcreek.com/java-api-examples/?api=com.firebase.ui.database.FirebaseRecyclerAdapter
#Override
public void onDataChanged()
{
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
if (getItemCount() == 0)
{
Log.d(TAG,"No data exists");
}
else
{
Log.d(TAG,"data exists");
}
}

rxJava Observer.onNext not called second time

I am using rxJava to fetch data from the database and show it in a recyclerview. The relevant code is shown below
function updateUI(){
ContactsLab contactsLab = ContactsLab.get(getActivity());
Subscription sub = contactsLab.getContactList().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.toList()
.subscribe(onContactsReceived());
mCompositeSubscription.add(sub);
}
ContactsLab is a singleton that returns an Observable of Contact objects.
onContactsReceived function is shown below
private Observer<List<Contact>> onContactsReceived(){
return new Observer<List<Contact>>() {
#Override
public void onCompleted() {}
#Override
public void onError(Throwable e) {}
#Override
public void onNext(List<Contact> contacts) {
if(mContactsAdapter == null) {
mContactsAdapter = new ContactsAdapter(contacts);
mRecyclerView.setAdapter(mContactsAdapter);
} else{
mContactsAdapter.setContactList(contacts);
mContactsAdapter.notifyDataSetChanged();
}
}
};
}
The updateUI function is called in my fragment onResume but the view is updated only the first time. If I come back to this fragment from any other fragment (having added more items to db), onResume is called, updateUI runs and onContactsReceived also runs but returns immediately without calling onNext or onComplete.
I think this has something to do with the way rxJava handles observables but no idea how to fix it (read about defer but couldn't understand much). Can somebody please help?
Edit:
The getContactList function look like this :
public rx.Observable<Contact> getContactList() {
List<Contact> contacts = new ArrayList<>();
ContactCursorWrapper cursorWrapper = queryContacts(null, null);
try{
cursorWrapper.moveToFirst();
while (!cursorWrapper.isAfterLast()){
contacts.add(cursorWrapper.getContact());
cursorWrapper.moveToNext();
}
} finally {
cursorWrapper.close();
}
return rx.Observable.from(contacts);
}
Basically it queries the database and maps the returned Cursor into my Contact class(which is a POJO). I added the rx.Observable.from to get an observable that was later collated using toList and updated into the adapter.
I used this approach avoid having to call notifyDataSetChanged after getting each item (and call it only once after getting all that).
What's the right approach to minimize the number of notifyDataSetChanged calls and also, refresh each time onResume is called?
Your observable contactsLab.getContactList().toList() has terminated.toList() collects all emissions from a source observable to a list and emits the entire list once the source Observable terminates (see the documentation). You aren't going to observe any more emissions from it.

RxJava with Firebase Remove child events

I'm using firebase to store my data and it is structured this way:
users {
userid {information here}
}
friends {
userid {
friendid1:true
friendid2:true
}
}
If a friendid is added or removed, then an appropriate event should be called.
If the user information is changed, then an event update should be triggered.
So far I have:
RxFirebaseDatabase.observeFriendList(/*Path to friends id list*/)
.flatMap(new Func1<RxFirebaseChildEvent, Observable<User>>() {
#Override
public Observable<User> call(RxFirebaseChildEvent stringBooleanPair) {
//This is a childeventlistener callback
//The key of RxFirebaseChildEvent is the friendid
switch (stringBooleanPair.getEventType()) {
case ADDED:
return RxFirebaseDatabase.observeUserInformation(/*path to user list*/.child(stringBooleanPair.getKey()), User.class);
case REMOVED:
return null; //What do I do here to unregister the listener?
}
}
}).subscribe(user -> {
//This is a ValueEventListener callback that returns the user
//Add, remove or update here, how?
});
How do I remove the specific user listener when a friendId is removed and also call add/remove/update on that user to update the view?
I came up with a somewhat hacked-together version, would love to see a better implementation though.
//refUsers is path to users
List<String> userKeys = new ArrayList<>();
RxFirebaseDatabase.observeFriendList(/*Path to friends id list*/)
.flatMap(new Func1<RxFirebaseChildEvent, Observable<User>>() {
#Override
public Observable<User> call(RxFirebaseChildEvent stringBooleanPair) {
//This is a childeventlistener callback
//The key of RxFirebaseChildEvent is the friendid
switch (stringBooleanPair.getEventType()) {
case ADDED:
userKeys.add(stringBooleanPair.getKey());//User Key
return RxFirebaseDatabase
.observeUserInformation(refUsers.child(stringBooleanPair.getKey()),
User.class,
Eventype.ADDED)
.takeWhile((user) -> userKeys.contains(user.getKey()));
case REMOVED:
userKeys.remove(userid);
return RxFirebaseDatabase
.observeUserInformation(refUsers.child(userid),
User.class,
Const.EventType.REMOVED)
.take(1);
}
}
}).subscribe(userEvent -> {
//Returns an object that contains the user and the eventype (added, updated or removed)
});
Basically, I'm keeping track of the users that are in the friends list and only returning an event if the userid is still watched.
You don't need to do that RxFirebase library already remove your listener when you unsubscribe your Observable, you can see it if you check the implementation:
subscriber.add(Subscriptions.create(new Action0() {
#Override
public void call() {
query.removeEventListener(childEventListener);
}
I suggest you to check as reference(or just use it) my RxJava2 library if you want to update your app from RxJava to RxJava2:
https://github.com/FrangSierra/Rx2Firebase
If you are interested of it, you can see the differences between both RxJava here.

RxJava one observable, multiple subscribers, one execution

I create an Observable from a long running operation + callback like this:
public Observable<API> login(){
return Observable.create(new Observable.OnSubscribe<API>() {
#Override
public void call(final Subscriber<? super API> subscriber) {
API.login(new SimpleLoginListener() {
#Override
public void onLoginSuccess(String token) {
subscriber.onNext(API.from(token));
subscriber.onCompleted();
}
#Override
public void onLoginFailed(String reason) {
subscriber.onNext(API.error());
subscriber.onCompleted();
}
});
}
})
}
A successfully logged-in api is the pre-condition for multiple other operations like api.getX(), api.getY() so I thought I could chain these operation with RxJava and flatMap like this (simplified): login().getX() or login().getY().
My biggest problem is now, that I don't have control over when login(callback) is executed. However I want to be able to reuse the login result for all calls.
This means: the wrapped login(callback) call should be executed only once. The result should then be used for all following calls.
It seems the result would be similar to a queue that aggregates subscribers and then shares the result of the first execution.
What is the best way to achieve this? Am I missing a simpler alternative?
I tried code from this question and experiemented with cache(), share(), publish(), refCount() etc. but the wrapped function is called 3x when I do this for all of the mentioned operators:
apiWrapper.getX();
apiWrapper.getX();
apiWrapper.getY();
Is there something like autoConnect(time window) that aggregates multiple successive subscribers?
Applying cache() should make sure login is only called once.
public Observable<API> login() {
return Observable.create(s -> {
API.login(new SimpleLoginListener() {
#Override
public void onLoginSuccess(String token) {
s.setProducer(new SingleProducer<>(s, API.from(token)));
}
#Override
public void onLoginFailed(String reason) {
s.setProducer(new SingleProducer<>(s, API.error()));
}
});
}).cache();
}
If, for some reason you want to "clear" the cache, you can do the following trick:
AtomicReference<Observable<API>> loginCache = new AtomicReference<>(login());
public Observable<API> cachedLogin() {
return Observable.defer(() -> loginCache.get());
}
public void clearLoginCache() {
loginCache.set(login());
}
Ok I think I found one major problem in my approach:
Observable.create() is a factory method so even if every single observable was working as intented, I created many of them. One way to avoid this mistake is to create a single instance:
if(instance==null){ instance = Observable.create(...) }
return instance

Gathering data from Firebase asynchronously: when is the data-set complete?

in a Firebase Android app that I'm currently developing I would like to provide an export feature. This feature should allow the user to export a set of data that is stored in Firebase.
My plan is to gather all required data into a intermediate object (datastructure) that can be (re-)used for multiple export types.
I am running into the issue that because of the flat Firebase data structure that I am using (as explained in https://www.firebase.com/docs/android/guide/structuring-data.html), it's difficult to know when all the data required for the export has been collected.
Example: when retrieving all objects that are referenced using 'indices' (name: key, value true), for each of these I set an addListenerForSingleValueEvent listener, but because this returns asynchronous, it's impossible to determine when all the indices are retrieved. This way it's not possible to determine the correct moment to start the export.
Who has best practices for coping with this?
Posting this to show a worked out example of the comment of #FrankvanPuffelen that seems to do the job quite well:
#Override
public void onDataChange(DataSnapshot indexListDataSnapshot) {
final long participantsRequired = indexListDataSnapshot.getChildrenCount();
for (DataSnapshot ds : indexListDataSnapshot.getChildren()) {
DataUtil.getParticipantByKey( mEventKey, ds.getKey() ).addListenerForSingleValueEvent( new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Participant p = dataSnapshot.getValue(Participant.class);
mParticipants.add( p );
if (participantsRequired == mParticipants.size()){
executeExport();
}
}
#Override
public void onCancelled(FirebaseError firebaseError) {
mListener.onDataLoadFailure( firebaseError.toException() );
}
});
}
}

Resources