23

With the upcoming RxJava2 release one of the important changes is that null is no longer accepted as a stream element, i.e. following code will throw an exception: Observable.just(null)

Honestly, I have mixed feelings about this change and part of me understands that it will enforce clean APIs, but I can see a number of use cases when this might be a problem.

For instance, in my app I have an in-memory cache:

@Nullable CacheItem findCacheItem(long id);

CacheItem might not be present in cache, so method might return null value.

The way it is used with Rx* - is as following:

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

So with this approach, I might get null in my stream which is totally valid situation, so it is handled properly on receiving side - let's say UI changes its state if item is not present in cache:

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

With RxJava2 I am no longer allowed to post null down the stream, so I either need to wrap my CacheItem into some other class and make my stream produce that wrapper instead or make quite big architectural changes.

Wrapping every single stream element into nullable counterpart doesn't look right to me.

Am I missing something fundamental here?

It seems like the situation like mine is quite popular, so Im curious what is the recommended strategy to tackle this problem given new "no null" policy in RxJava2?

EDIT Please see follow-up conversation in RxJava GitHub repo

Pavel Dudka
  • 20,754
  • 7
  • 70
  • 83

5 Answers5

18

Well, there are several ways to represent what you want.

One option is to use 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();
}

You then use .compose(deoptionalize()) to map from the optional to the non-optional Observable.

whlk
  • 15,487
  • 13
  • 66
  • 96
Tassos Bassoukos
  • 16,017
  • 2
  • 36
  • 40
2

As another solution you can add static instance CacheItem.NULL, and return it to subscriber when there is no cached data

Single
    .concat(loadFromMemory(), loadFromDb(), loadFromServer())
    .takeFirst { it != CachedItem.NULL }
    .subscribe(
cVoronin
  • 1,341
  • 15
  • 21
2

You can use RxJava2-Nullable to handle null value in RxJava2.

For your situation, you can do:

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

To invoke showPrompt when it's null, you can do:

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();
}
Dean Xu
  • 4,438
  • 1
  • 17
  • 44
1

Possible solution is to use Maybe.switchIfEmpty

Example:

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());
Eugene Popovich
  • 3,343
  • 2
  • 30
  • 33
0

Nulls are weird things. From one side, they do not mark a full success, like a concrete value does. From the other side, they don't signal a complete failure like Exception does. So one can say that null is halfway between happy path and failure path. Whenever null is forbidden it pressures you to "make up your mind" and move it to one of the groups. Either make it a full-blown success (by wrapping it into some kind of object and putting the null handling logic into this object) or a complete error and throw it as an Exception.

To me it turned out very beneficial to limit the happy path to full success and handle all deviations in the onError channel:

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

The best part is that onError short-circuits all the transforms (which usually depend on the value being non-null) all the way to the point when you decide you can handle it. Being it either in subscribe/onError at the end or onErrorResumeNext somewhere in the middle of the chain. Keep in mind thought that allowing onError to propagate all the way terminates the chain.

Agent_L
  • 4,960
  • 28
  • 30