Brian Begun
Brian Begun

Reputation: 539

Trigger a second query from LiveData and merge results (Firestore)?

I have two collections: Users and Books. I need to get the results of both of them whether Users OR Books is updated and then merge the results together into a LinkedHashMap to use as a listView menu.

I thought a MediatorLiveData would be the way to go, but if I put the query of Users and the Query of Books in then I get null from one of the two LiveData objects because only one or the other fires. I thought maybe if one of them fires, then perhaps I have a query run inside each addSource() in the MediatorLiveData, but I'm not sure if that's the way to go.

My post regarding the MediatorLiveData is here: Using MediatorLiveData to merge to LiveData (Firestore) QuerySnapshot streams is producing weird results

My two queries and LiveData objects are as follows:

//getUsers query using FirebaseQueryLiveData class
private Query getUsersQuery() {
    FirebaseAuth mAuth = FirebaseAuth.getInstance();

    adminID = mAuth.getUid();
    query = FirebaseFirestore.getInstance().collection("admins")
            .document(adminID)
            .collection("users")
    return query;
}

private FirebaseQueryLiveData usersLiveData = new FirebaseQueryLiveData(getUsersQuery());


//getBooks query using FirebaseQueryLiveData class
private Query getBooksQuery () {
    FirebaseGroupID firebaseGroupID = new FirebaseGroupID(getApplication());
    groupID = firebaseGroupID.getGroupID();
    query = FirebaseFirestore.getInstance().collection("books")
            .whereEqualTo("groupID", groupID)      
    return query;
}

private FirebaseQueryLiveData booksLiveData = new FirebaseQueryLiveData(getBooksQuery()); 

Somehow when Users updates, I need to get the data of Books as well and then merge them, but I also need this to happen if Books updates and then get the data of Users and merge them.

Any ideas would be greatly appreciated.

Additional Note/Observation Okay, so I'm not completely ruling out a MediatorLiveData object. Certainly it allows me the listening of two different LiveData objects within the same method, however, I don't want to merge the two of them directly because I need to act on each liveData object individually. So as an example: usersLiveData fires because we create or modify a user, then I need to query books, get the results and merge users and books etc.

Below is my MediatorLiveData as it currently stands:

//MediatorLiveData merge two LiveData QuerySnapshot streams
private MediatorLiveData<QuerySnapshot> usersBooksLiveDataMerger() {
    final MediatorLiveData<QuerySnapshot> mediatorLiveData = new MediatorLiveData<>();
    mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
        @Override
        public void onChanged(@Nullable QuerySnapshot querySnapshot) {
            mediatorLiveData.setValue(querySnapshot);
        }
    });
    mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
        @Override
        public void onChanged(@Nullable QuerySnapshot querySnapshot) {
            mediatorLiveData.setValue(querySnapshot);
        }
    });
    return mediatorLiveData;
}

Right now it's returning null results of the other LiveData source. Instead I need to query then merge. Any ideas on how to do this? There isn't much out there on this very thing.

I tried putting a query inside a Function that is called using a Transformations.map() but because of it be an asynchronous call, the return statement is being called before the query finishes.

Here's my attempt at the Function:

    private class ListenUsersGetBooks implements Function<QuerySnapshot, LinkedHashMap<User, List<Book>>> {

        @Override
        public LinkedHashMap<User, List<Book>> apply(final QuerySnapshot input) {
            userBookList = new LinkedHashMap<>();
            getBooksQuery().get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    List<User> users = input.toObjects(User.class);
                    List<Book> books = task.getResult().toObjects(Book.class);
                    Log.d(TAG, "USERLIST! " + users);
                    Log.d(TAG, "BOOKLIST! " + books);
                    for (User user : users) {
                        bookList = new ArrayList<>();
                        for (Book book : books) {
                            if (user.getUserID().equals(book.getUserID())
                                    && book.getBookAssigned()) {
                                bookList.add(book);
                            }
                            else if (user.getAllBookID().equals(book.getBookID())) {
                                bookList.add(book);
                            }
                        }
                        userBookList.put(user, bookList);
                    }
                    Log.d(TAG,"OBSERVE userBookList: " + userBookList);
                }
            });
            return userBookList;
        }
    } 

Upvotes: 5

Views: 2300

Answers (4)

zOqvxf
zOqvxf

Reputation: 1579

You can greatly simplify the usage by using my LiveDataZipExtensions https://gist.github.com/Benjiko99/d2e5406aab0a4a775ea747956ae16624

With them, you don't have to create an object to hold your combined result.

Example usage

val firstNameLD = MutableLiveData<String>().apply { value = "John" }
val lastNameLD = MutableLiveData<String>().apply { value = "Smith" }

// The map function will get called once all zipped LiveData are present
val fullNameLD = zip(firstNameLD, lastNameLD).map { (firstName, lastName) ->
    "$firstName $lastName"
}

Upvotes: 0

Brian Begun
Brian Begun

Reputation: 539

I think I solved it. We were declaring a new MyResult object in each mediatorLiveData.addSource() method. Which meant that we were getting a new object for each QuerySnapshot so we would never get them to merge with each other.

Here's the update to MediatorLiveData:

    private MediatorLiveData<MyResult> usersBooksLiveDataMerger() {
        final MediatorLiveData<MyResult> mediatorLiveData = new MediatorLiveData<>();
        final MyResult current = new MyResult();
        mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
            @Override
            public void onChanged(@Nullable QuerySnapshot querySnapshot) {
                current.setUsersSnapshot(querySnapshot);
                mediatorLiveData.setValue(current);
            }
        });
        mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
            @Override
            public void onChanged(@Nullable QuerySnapshot querySnapshot) {
                current.setBooksSnapshot(querySnapshot);
                mediatorLiveData.setValue(current);
            }
        });
        return mediatorLiveData;
    } 

Now I'm getting users and books in the observer in Activity! Now the only thing I need to do is transform (merge the data) into a LinkedHashMap, but I think I got that figured out. Thanks Sam!

Upvotes: 2

Brian Begun
Brian Begun

Reputation: 539

So this is where I am with your suggestions Sam.

I added getter and setter methods to the MyResult class as it wasn't giving me access to the member variables in the observer otherwise:

    public class MyResult {

        QuerySnapshot usersSnapshot;
        QuerySnapshot booksSnapshot;

        //default constructor
        public MyResult() {
        }

        public QuerySnapshot getUsersSnapshot() {
            return usersSnapshot;
        }

        public void setUsersSnapshot(QuerySnapshot usersSnapshot) {
            this.usersSnapshot = usersSnapshot;
        }

        public QuerySnapshot getBooksSnapshot() {
            return booksSnapshot;
        }

        public void setBooksSnapshot(QuerySnapshot booksSnapshot) {
            this.booksSnapshot = booksSnapshot;
        }

        public boolean isComplete() {
            return (usersSnapshot != null && booksSnapshot != null);
        }
    } 

Here's the MediatorLiveData and get method. I changed the MyResult class initialization to = new MyResult(); thinking there was an issue with using mediatorLiveData.getValue(); as the initialization and get method.

private MediatorLiveData<MyResult> usersBooksLiveDataMerger() {
    final MediatorLiveData<MyResult> mediatorLiveData = new MediatorLiveData<>();
    mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
        @Override
        public void onChanged(@Nullable QuerySnapshot querySnapshot) {
            MyResult current = new MyResult();
            current.setUsersSnapshot(querySnapshot);
            mediatorLiveData.setValue(current);
        }
    });
    mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
        @Override
        public void onChanged(@Nullable QuerySnapshot querySnapshot) {
            MyResult current = new MyResult();
            current.setBooksSnapshot(querySnapshot);
            mediatorLiveData.setValue(current);
        }
    });
    return mediatorLiveData;
}

public MediatorLiveData<MyResult> getUsersBooksLiveDataMerger() {
    return usersBooksLiveDataMerger();
}

And finally the observer:

    mainViewModel.getUsersBooksLiveDataMerger().observe(this, new Observer<MainViewModel.MyResult>() {
            @Override
            public void onChanged(@Nullable MainViewModel.MyResult myResult) {
                if (myResult == null || !myResult.isComplete()) {
                    // Ignore, this means only one of the queries has fininshed
                    Log.d(TAG, "OBSERVE BLAH!!!!");
                    return;
                }

                // If you get to here, you know all the queries are ready!
                // ...
                List<Book> books;
                List<User> users;
                books = myResult.getBooksSnapshot().toObjects(Book.class);
                users = myResult.getUsersSnapshot().toObjects(User.class);
                Log.d(TAG, "OBSERVE MERGE users: " + users);
                Log.d(TAG, "OBSERVE MERGE books: " + books);
            }
        }); 

Please note: I did do a null check in the mediatorLiveData, just took it out for testing purposes.

Somehow I need to trigger my books query if just my users is triggered AND I need to trigger my users query if just my books is triggered...I feel like there is a step before the MediatorLiveData that needs to happen so we can make sure one liveData triggers the other query. Does that make sense?

Upvotes: 0

Sam Stern
Sam Stern

Reputation: 25134

Here's a simple version of what you could do, I hope it makes sense.

You're close with the MediatorLiveData. Instead of MediatorLiveData<QuerySnapshot> you probably want to use a custom object like this:

class MyResult {

  public QuerySnapshot usersSnapshot;
  public QuerySnapshot booksSnapshot;

  public MyResult() {}

  boolean isComplete() {
    return (usersSnapshot != null && booksSnapshot != null);
  }
}

Then in your observers, do something like this:

private MediatorLiveData<MyResult> usersBooksLiveDataMerger() {
    final MediatorLiveData<MyResult> mediatorLiveData = new MediatorLiveData<>();
    mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
        @Override
        public void onChanged(@Nullable QuerySnapshot querySnapshot) {
            MyResult current = mediatorLiveData.getValue();
            current.usersSnapshot = querySnapshot;
            mediatorLiveData.setValue(current);
        }
    });
    mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
        @Override
        public void onChanged(@Nullable QuerySnapshot querySnapshot) {
            MyResult current = mediatorLiveData.getValue();
            current.booksSnapshot = querySnapshot;
            mediatorLiveData.setValue(current);
        }
    });
    return mediatorLiveData;
}

Then when you observe the combined live data:

usersBooksLiveDataMerger().observe(new Observer<MyResult>() {

    @Override
    public void onChanged(@Nullable MyResult result) {
        if (result == null || !result.isComplete()) {
          // Ignore, this means only one of the queries has fininshed
          return;
        }

        // If you get to here, you know all the queries are ready!
        // ...
    }

});

Upvotes: 7

Related Questions