Reputation: 539
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
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
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
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
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