Обработка null в RxJava2

#rx-java

#rx-java

Вопрос:

С предстоящим выпуском RxJava2 одним из важных изменений является то, что null он больше не принимается в качестве элемента потока, т.Е. Следующий код будет генерировать исключение: Observable.just(null)

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

Например, в моем приложении у меня есть кэш в памяти:

 @Nullable CacheItem findCacheItem(long id);
  

CacheItem может отсутствовать в кэше, поэтому метод может возвращать нулевое значение.

Способ его использования с Rx * — заключается в следующем:

 Observable<CacheItem> getStream(final long id) {
    return Observable.fromCallable(new Callable<CacheItem>() {
        @Override public CacheItem call() throws Exception {
            return findCacheItem(id);
        }
    });
}
  

Таким образом, при таком подходе я могу получить null в своем потоке, что является полностью допустимой ситуацией, поэтому оно обрабатывается должным образом на принимающей стороне — допустим, пользовательский интерфейс изменяет свое состояние, если элемент отсутствует в кэше:

 Observable.just(user)
          .map(user -> user.getName())
          .map(name -> convertNameToId(name))
          .flatMap(id -> getStream(id))
          .map(cacheItem -> getUserInfoFromCacheItem(cacheItem))
          .subscribe(
              userInfo -> {
                  if(userInfo != null) showUserInfo();
                  else showPrompt();
              }
          );
  

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

Перенос каждого отдельного элемента потока в обнуляемый аналог мне кажется неправильным.

Я упускаю что-то фундаментальное здесь?

Похоже, что ситуация, подобная моей, довольно популярна, поэтому мне любопытно, какова рекомендуемая стратегия решения этой проблемы, учитывая новую политику «no null» в RxJava2?

РЕДАКТИРОВАТЬ Пожалуйста, смотрите последующее обсуждение в репозитории RxJava на GitHub

Ответ №1:

Ну, есть несколько способов представить то, что вы хотите.

Одним из вариантов является использование Observable<Optional<CacheItem>> :

 Observable<Optional<CacheItem>> getStream(final long id) {
  return Observable.defer(() -> {
    return Observable.just(Optional.ofNullable(findCacheItem(id)));
  });
}

public static <T> Transformer<Optional<T>, T> deoptionalize() {
  return src -> 
      src.flatMap(item -> item.isPresent()
             ? Observable.just(item.get())
             : Observable.empty();
}
  

Затем вы используете .compose(deoptionalize()) для сопоставления необязательного с необязательным наблюдаемым.

Ответ №2:

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

 Single
    .concat(loadFromMemory(), loadFromDb(), loadFromServer())
    .takeFirst { it != CachedItem.NULL }
    .subscribe(
  

Ответ №3:

Вы можете использовать RxJava2-Nullable для обработки нулевого значения в RxJava2.

Для вашей ситуации вы можете сделать:

 Observable<CacheItem> getStream(final long id) {
    return RxNullable.fromCallable(() -> findCacheItem(id))
                     .onNullDrop()
                     .observable();
}
  

Чтобы вызвать showPrompt , когда оно равно null, вы можете сделать:

 Observable.just(user)
          .map(user -> user.getName())
          .map(name -> convertNameToId(name))
          .flatMap(id -> getStream(id).onNullRun(() -> showPrompt()))
          .map(cacheItem -> getUserInfoFromCacheItem(cacheItem))
          .subscribe(userInfo -> showUserInfo());

NullableObservable<CacheItem> getStream(final long id) {
    return RxNullable.fromCallable(() -> findCacheItem(id)).observable();
}
  

Ответ №4:

Возможное решение — использовать Maybe.switchIfEmpty

Пример:

 public static <T> Maybe<T> maybeOfNullable(T value) {
    return value == null ? Maybe.empty() : Maybe.just(value);
}

maybeOfNullable(user)
        .map(user -> user.getName())
        .map(name -> convertNameToId(name))
        .flatMap(id -> getStream(id))
        .map(cacheItem -> getUserInfoFromCacheItem(cacheItem))
        // perform another action in case there are no any non null item emitted
        .switchIfEmpty(Maybe.fromAction(() -> showPrompt()))
        .subscribe(userInfo -> showUserInfo());
  

Ответ №5:

Nulls странные вещи. С одной стороны, они не отмечают полный успех, как это делает конкретное значение. С другой стороны, они не сигнализируют о полном сбое, как Exception это происходит. Таким образом, можно сказать, что null это на полпути между счастливым путем и путем отказа. Всякий null раз, когда это запрещено, это заставляет вас «принять решение» и переместить его в одну из групп. Либо сделайте это полноценным успехом (обернув его в какой-то объект и поместив логику обработки null в этот объект), либо полной ошибкой и выбросьте ее как Exception .

Для меня оказалось очень выгодно ограничить счастливый путь к полному успеху и обрабатывать все отклонения в onError канале:

 Observable.just(user)
      .map(user -> user.getName())
      .map(name -> convertNameToId(name))
      .flatMap(id -> getStream(id))
      .map(cacheItem -> getUserInfoFromCacheItem(cacheItem)) //throws custom exception
      .subscribe(
          userInfo -> {
              showUserInfo(userInfo);
          },
          ex -> {
              showPrompt(ex); //handles the custom exception
          }
      );
  

Лучшая часть заключается в том, что onError происходит короткое замыкание всех преобразований (которые обычно зависят от значения, отличного от null) вплоть до момента, когда вы решите, что справитесь с этим. Это либо subscribe/onError в конце, либо onErrorResumeNext где-то в середине цепочки. Имейте в виду, что разрешение onError распространяться полностью завершает цепочку.