Запустить второй запрос из LiveData и объединить результаты (Firestore)?

У меня две коллекции: Пользователи и Книги. Мне нужно получить результаты обоих из них, обновляются ли пользователи или книги, а затем объединить результаты вместе в LinkedHashMap для использования в качестве меню listView.

Я думал, что MediatorLiveData будет подходящим вариантом, но если я помещу запрос Users и Query of Books, тогда я получу null от одного из двух объектов LiveData, потому что срабатывает только один или другой. Я подумал, может быть, если один из них сработает, то, возможно, у меня будет выполняться запрос внутри каждого addSource () в MediatorLiveData, но я не уверен, что это правильный путь.

Мой пост о MediatorLiveData находится здесь: Использование MediatorLiveData для слияния с потоками LiveData (Firestore) QuerySnapshot дает странные результаты

Мои два запроса и объекты LiveData следующие:

//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()); 

Каким-то образом, когда пользователи обновляются, мне также нужно получить данные о книгах, а затем объединить их, но мне также нужно, чтобы это произошло, если книги обновляются, а затем получают данные пользователей и объединяют их.

Любые идеи очень приветствуются.

Дополнительное примечание / наблюдение Итак, я не исключаю полностью объект MediatorLiveData. Конечно, это позволяет мне прослушивать два разных объекта LiveData в одном методе, однако я не хочу объединять два из них напрямую, потому что мне нужно действовать с каждым объектом liveData индивидуально. Итак, в качестве примера: usersLiveData срабатывает, потому что мы создаем или изменяем пользователя, затем мне нужно запрашивать книги, получать результаты и объединять пользователей и книги и т. д.

Ниже представлены мои данные MediatorLiveData в их нынешнем виде:

//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;
}

Прямо сейчас он возвращает нулевые результаты другого источника LiveData. Вместо этого мне нужно запросить, а затем слить. Есть идеи, как это сделать? По этому поводу не так много информации.

Я попытался поместить запрос внутри функции, которая вызывается с помощью Transformations.map (), но из-за того, что это асинхронный вызов, оператор return вызывается до завершения запроса.

Вот моя попытка использования функции:

    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;
        }
    } 
4
0
1 926
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Вот где я нахожусь с вашими предложениями, Сэм.

Я добавил методы получения и установки в класс MyResult, поскольку в противном случае он не давал мне доступа к переменным-членам в наблюдателе:

    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);
        }
    } 

Вот метод MediatorLiveData и get. Я изменил инициализацию класса MyResult на = new MyResult (); думая, что возникла проблема с использованием mediatorLiveData.getValue (); как метод инициализации и получения.

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();
}

И, наконец, наблюдатель:

    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);
            }
        }); 

Обратите внимание: я сделал нулевую проверку в mediatorLiveData, просто взял его для тестирования.

Каким-то образом мне нужно инициировать запрос моих книг, если запускаются только мои пользователи, И мне нужно инициировать запрос моих пользователей, если запускаются только мои книги ... Я чувствую, что есть шаг до MediatorLiveData, который должен произойти, чтобы мы могли убедиться один liveData запускает другой запрос. Имеет ли это смысл?

Ответ принят как подходящий

Вот простая версия того, что вы могли бы сделать, я надеюсь, это имеет смысл.

Вы близки с MediatorLiveData. Вместо MediatorLiveData<QuerySnapshot> вы, вероятно, захотите использовать такой настраиваемый объект:

class MyResult {

  public QuerySnapshot usersSnapshot;
  public QuerySnapshot booksSnapshot;

  public MyResult() {}

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

Затем в ваших наблюдателях сделайте что-то вроде этого:

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;
}

Затем, когда вы наблюдаете за объединенными данными в реальном времени:

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!
        // ...
    }

});

Спасибо, Сэм! Я очень ценю руководство. Я пробовал ваш образец кода, но получаю сообщение об ошибке несовместимых типов: MyResult current = mediatorLiveData.getValue (); Мы устанавливаем QuerySnapshot на Class MyResult, и Android это не нравится.

Brian Begun 25.05.2018 17:47

Вы правы: это должен быть MediatorLiveData <MyResult>

Sam Stern 25.05.2018 17:56

Также я не имел в виду, чтобы этот код был готов для копирования и вставки: обязательно проверьте наличие null, когда это необходимо!

Sam Stern 25.05.2018 17:57

Конечно. Спасибо за исправление. Я настраиваюсь с вашей отправной точки. На данный момент я не получаю никаких результатов в обозревателе. Я посмотрю, смогу ли я определить, где что-то идет не так.

Brian Begun 25.05.2018 18:09

Хорошо, вот проблема, с которой я столкнулся. QuerySnapshots запускаются так, как должны. Однако проблема в том, что мне нужны результаты обоих запросов, даже если срабатывает только один. Из-за того, как настроен ваш образец, я никогда не получу полных результатов, потому что скорее всего сработает только один из них. Я могу редактировать / добавлять пользователя, но когда это происходит, мне все равно нужно запрашивать книги. Я что-то пропустил?

Brian Begun 25.05.2018 18:51

Я чувствую, что мне нужно как-то вызвать другой запрос, когда один срабатывает, ТОГДА объедините их вместе, используя MediatorLiveData.

Brian Begun 25.05.2018 19:24

Хорошо, я вернулся и снова прочитал документы LiveData. Думаю, я понимаю, что для этого должен работать MediatorLiveData. Поскольку LiveData будет обновляться, когда жизненный цикл станет активным или после изменения состояния, как описано в документации. Итак, мне просто нужно выяснить, почему я не передаю данные наблюдателю.

Brian Begun 25.05.2018 23:22

Да, точно. Данные начинают поступать, когда есть активный наблюдатель. Добавление наблюдателя к посреднику также активирует «дочерние» источники.

Sam Stern 26.05.2018 00:28

Несмотря на то, что мне пришлось внести некоторые коррективы в ваш ответ, Сэм, вы помогли мне сузить круг моей проблемы, поэтому я сделаю ваш ответ как САМЫЙ. Еще раз спасибо, @Sam Stern!

Brian Begun 26.05.2018 21:41

Думаю, я решил это. Мы объявляли новый объект MyResult в каждом методе mediatorLiveData.addSource (). Это означало, что мы получали новый объект для каждого QuerySnapshot, чтобы мы никогда не смогли заставить их объединиться друг с другом.

Вот обновление для 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;
    } 

Теперь получаю пользователей и книги в наблюдателе в Activity! Теперь единственное, что мне нужно сделать, это преобразовать (объединить данные) в LinkedHashMap, но я думаю, что понял это. Спасибо, Сэм!

Вы можете значительно упростить использование, используя мой LiveDataZipExtensionshttps://gist.github.com/Benjiko99/d2e5406aab0a4a775ea747956ae16624

С ними вам не нужно создавать объект для хранения вашего комбинированного результата.

Пример использования

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"
}

Другие вопросы по теме