Communicating between the UI and the service layer (database, web service, etc) asynchronously is a common challenge for Android apps. One great way to address this issue is to use an Rx Observable:

webservice.getUser(userId)
        .subscribeOn(AndroidSchedulers.mainThread())
        .subscribe(user -> {
            // onNext: do something with user object
        }, throwable -> {
            // onError: display possible error
        });

In these situations we often want to alert the user when this operation is “in progress.” We may use a loading indicator on a button or a full screen loading indicator. Whatever we chose it will be some temporary visual cue that async operation is still happening. We will show and hide the indicator within our Observable pipe.

// display loading indicator
webservice.getUser(userId)
        .subscribeOn(AndroidSchedulers.mainThread())
        .subscribe(user -> {
            // onNext: do something with user object
            // dismiss loading indicator
        }, throwable -> {
            // onError: display possible error
            // dismiss loading indicator
        });

Because of the async nature of the operation, there is no way to know ahead of time how long it will take to complete. This web service example may finish in milliseconds on a fast connection or may extend until the request times out on slower connections. For users with a slow connection, this temporary visual change is easy to process. It comes on screen, stays long enough to be registered by the brain and then fades away.

But what about users with fast connections? If we display a loading indicator and only a few tenths of a second pass before the web request finishes, the user will experience a flicker of the loading indicator. The limitations of human visual recognition are well known. By not giving the brain time to process the visual change, we risk creating a jarring and unpleasant user experience. This is oddly counterintuitive: the user with the better network connection gets the worse user experience.

One easy way to mitigate this problem is to display the loading indicator for a minimum amount of time (for example one second) no matter how long the underlying async operation takes. So if the request only takes 200 milliseconds, the user will still see the indicator for one second. For any request longer than a second, the user will see the indicator the entire time. In this case, the user will always have time to visually process the appearance of the loading indicator. However, we don’t want to extend it too far (say five seconds) because we risk going too long and actually frustrating the user by simulating a slow connection.

So, how can we easily integrate a timer like this into our Observable above? An initial idea might be to use a delayed runnable to dismiss the indicator:

// display loading indicator
Handler handler = new Handler(Looper.myLooper());

webservice.getUser(userId)
              .subscribeOn(AndroidSchedulers.mainThread())
              .subscribe(user -> {
                  handler.postDelayed(() -> {
                      // onNext: do something with user object
                      // dismiss loading indicator
                  }, 1000);
              }, throwable -> {
                  handler.postDelayed(() -> {
                      // onError: display possible error
                      // dismiss loading indicator
                  }, 1000);
              });

The initial problem is that we are adding one second to all web requests, for our fast connection and slow connection users. This is benefiting one group at the expense of the other. Also, this isn’t very elegant and requires us to post a delay in two places (onNext and onError) for every async operation in our entire app. We can DRY this up, clean things up, and ditch the Handler by using two Rx operators: timer and zip.

The zip operator allows us to take the output of two or more Observables, perform a function on those outputs and then return a single stream. The cool part for us is that zip waits for each input Observable to emit an item before emitting an item itself. You can experiment with the zip operator with this awesome interactive tool.

The timer operator is pretty straight forward. It emits a single item after a specified time. This is a single shot operator, unlike interval which endlessly emits items while subscribed to.

So how can we use zip and timer together to get what we want? We can zip together our initial web request Observable with a timer. This will result in an Observable that emits items of the same type as our initial Observable but only after a delay specified by the timer. By using the zip operator, both the timer and the web request have to complete before posting an item to onNext.

This works great with fast web requests. Any web request that finishes in less than a second will wait for the timer to finish and then emit the response. This works great with slow requests too. For any request that takes longer than a second, the timer will complete and wait for the web request to finish before emitting an item.

Observable userObservable = webservice.getUser(userId);
Observable<Long> timer = Observable.timer(1000, TimeUnit.MILLISECONDS);
return Observable.zip(userObservable, timer, ...);

The only thing to consider now is the function we perform on the combined results. Here we simply return the result of the web request Observable and ignore the timer value:

Observable<User> userObservable = webservice.getUser(userId);
Observable<Long> timer = Observable.timer(1000, TimeUnit.MILLISECONDS);
Observable.zip(userObservable, timer, new Func2<User, Long, User>() {
    @Override
    public User call(User user, Long timerValue) {
        return user; // emit the user; ignore the timer value
    }
});

We can easily lambda-ify this and add our subscribe statement back in:

// display loading indicator
Observable<User> userObservable = webservice.getUser(userId);
Observable<Long> timer = Observable.timer(1000, TimeUnit.MILLISECONDS);
Observable.zip(userObservable, timer, (user, timerValue) -> user)
          .subscribeOn(AndroidSchedulers.mainThread())
          .subscribe(user -> {
                // onNext: do something with user object
                // dismiss loading indicator
          }, throwable -> {
                // onError: display possible error
                // dismiss loading indicator
          });

We’ve really cleaned things up by removing our Handler and making this work for fast and slow connections but we haven’t DRYed this up yet. To do that we just extract this code into a helper function:

public static <T> Observable<T> zipWithTimer(Observable<T> observable) {
    return Observable.zip(observable,
            Observable.timer(1000, TimeUnit.MILLISECONDS), (t, timerValue) -> t);
}

and call it as such:

// display loading indicator
zipWithTimer(webservice.getUser(userId))
    .subscribeOn(AndroidSchedulers.mainThread())
    .subscribe(user -> {
        // onNext: do something with user object
        // dismiss loading indicator
    }, throwable -> {
        // on Error
        // dismiss loading indicator
    });

In the end we replaced:

webservice.getUser(userId)

with

zipWithTimer(webservice.getUser(userId))

We can now go and refactor our entire app. Adding the loading indicator UI elements and hide/show code will obviously require some work but the Observable mechanism will be easy. I’ve added this code into a helper class. Feel free to use it and let me know how it works for you.

import java.util.concurrent.TimeUnit;
import rx.Observable;

public final class ObservableUtils {

    /**
     * Zip the input observable with a timer.
     * This will return an observable that only emits items from the input observable
     * once BOTH an item is emitted from the input observable AND the timer expires.
     */
    public static <T> Observable<T> zipWithTimer(Observable<T> observable) {
        return Observable.zip(observable,
                Observable.timer(1000, TimeUnit.MILLISECONDS), (t, timerValue) -> t);
    }
}

One thing to note with this approach is that if the input Observable (the web request in our example) errors out (i.e. would call onError), the zip will not wait for the timer to complete. Our final Observable returned from zipWithTimer may receive onError in less than one second which would still result in a flicker of the loading indicator.

Thanks to Ross Hambrick, who provided genesis of these ideas.

Leave a Reply to David Cancel reply

Your email address will not be published. Required fields are marked *