Adel Nizamutdinov

I wanted to share this

RxJava on Android: PopupMenus and Dialogs

This post is about how to treat PopupMenus and Dialogs as ordinary events, and being able to put them into your RxJava event pipelines. I’m assuming that you’re pretty comfortable with RxJava, so I won’t go into all the deepest details.

PopupMenus

PopupMenu

So the regular way to show a PopupMenu on a View is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
view.setOnClickListener(v -> {
  final PopupMenu menu = new PopupMenu(context, v);
  menu.inflate(R.menu.xxx);
  menu.setOnMenuItemClickListener(item -> {
    switch (item.getItemId()) {
      case R.id.xxx1:
        // do 1
        break;
      case R.id.xxx2:
        // do 2
        break;
      case R.id.xxx3:
        // do 3
        break;
    }
    return true;
  });
  menu.show();
});

But the menu click is an event – let’s represent it with an Observable<MenuItem> that will emit exactly one item – a menu item click:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Observable<MenuItem> popupMenu(View view, int menuRes) {
  return Observable.create((Subscriber<? super MenuItem> subscriber) -> {
    final PopupMenu menu = new PopupMenu(view.getContext(), view);
    menu.inflate(menuRes);

    // cleaning up in case of unsubscribe() call
    subscriber.add(Subscriptions.create(() -> {
      menu.setOnMenuItemClickListener(null);
      menu.dismiss();
    }));

    menu.setOnMenuItemClickListener(item -> {
      subscriber.onNext(item);
      // PopupMenu always emits exactly one item
      subscriber.onCompleted();
      return true;
    });
    menu.show();
  });
}

So what can we do with this? Compose-compose-compose!

1
2
3
4
5
6
7
8
9
10
11
12
ViewObservable.clicks(view)
    .flatMap(v -> popupMenu(v, R.menu.xxx))
    .subscribe(item -> {
      switch (item.getItemId()) {
        case R.id.xxx1:
          // do 1
          break;
        case R.id.xxx2:
          // do 2
          break;
      }
    });

We flatMap each View click into a PopupMenu item click, and get MenuItem at the end of our stream. We can even go further (and in fact we should, because MenuItem is never what we want in the end) and flatMap them into some async REST API call, or filter them by id. The possibilities are endless.

Dialogs

Dialog

Exactly the same story here: in the simpliest case Dialog is just an Observable<Boolean> where true stands for clicking on Accept and false for clicking on Decline

So this is the function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Observable<Boolean> dialog(Context context, int title, int message) {
  return Observable.create((Subscriber<? super Boolean> subscriber) -> {
    final AlertDialog ad = new AlertDialog.Builder(context)
        .setTitle(title)
        .setMessage(message)
        .setPositiveButton(android.R.string.ok, (dialog, which) -> {
          subscriber.onNext(true);
          subscriber.onCompleted();
        })
        .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
          subscriber.onNext(false);
          subscriber.onCompleted();
        })
        .create();
    // cleaning up
    subscriber.add(Subscriptions.create(ad::dismiss));
    ad.show();
  });
}

Now you can use it exactly the same way you use your ordinary Observable<>s:

1
2
3
4
5
6
7
clicks
  .flatMap(x -> dialog(context, title, message))
  .filter(x -> x == true) // only 'Accept' clicks
  .flatMap(x -> api.confirmSomething().subscribeOn(Schedulers.io()))
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(x -> // do stuff,
             e -> // recover);

Also remember, you can represent any of your custom Dialogs as any type of Observable<>. If you have some kind of Date picker, it can be Observable<Date>, if it’s a captcha Dialog, you can represent it as an Observable<String>. And this is when RxJava REALLY starts to shine.

Suppose we have some api call

1
2
3
4
5
6
7
8
9
10
11
Observable<Data> apiCall(Arg arg);

class Data {
  Result result;
  Error error;
}

class Error extends Throwable {
  boolean isCaptcha();
  String captchaImgUrl;
}

And a captcha dialog:

1
Observable<String> captchaDialog(String captchaImgUrl);

And we want to abstract away from Captchas, and never worry about them again, here’s what we can do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiClient.apiCall(arg)
  .flatMap(x -> x.result != null
             ? Observable.just(x.result)
             : x.error.isCaptcha()
               ? captchaDialog(x.error.captchaImgUrl)
                   // send the captcha
                   .flatMap(str -> apiClient.submitCaptcha(str))
                   // call the api method again
                   // this should be done recursively
                   // simplicity for the sake of blog post
                   .flatMap(success -> apiClient.apiCall(arg))
               : Observable.error(x.error))
  .subscribe(x -> // at this point we recovered from any captcha requests we encountered, and got our desired data,
             e -> // failure);

My favourite part about RxJava is that it allows to abstract over all this event-based and async failure situations that we can recover from, or maybe ask the user for assistance. And when you abstract from all this failure, programming becomes such a breeze.

Comments