読者です 読者をやめる 読者になる 読者になる

Android で複数非同期処理のシーケンス制御をライブラリに依存せず Pure Java で実装しなければならない場合の一案

もし、 Android 開発において、ライブラリに依存せず Pure Java を前提とし、複数の非同期処理をシーケンス制御する必要が出てきた場合に、どう解決すればよいか、の問題に対する一案を残した記録文章です。

背景

直近の仕事で、 Android 向け社内ライブラリの開発に携わっていたのですが、いくつかの社内事情があり、そのライブラリが依存するライブラリは appcompat-v7 のみという制約がありました。

f:id:sho5nn:20170506202536j:plain

あまり詳しくは書けないですが、そのライブラリは SSO を実現する機能を備えていて、いくつかのログインシーケンス制御があり、そのシーケンス中にあるそれぞれのステップが非同期処理で実行され、前の非同期処理の結果を元に次の非同期処理を行うような処理を実装する必要がありました。

最近のトレンドに習えば、 RxJava ライブラリなどを使用して精神的安定を維持しつつ開発・保守したいところですが、先述した制約のため Pure Java で実装するしかない状態でした。

その状態のまま開発した場合は言わずもがな、先行き怪しく最悪死人も待ったなしなのでどうにかしないとその先は地獄だぞという気持ちがあり、他のメンバーと議論したところ、 JavaScript の Promise からヒントを得てみてはどうだろうかということになり、 Promise like なインターフェースをまず実装し、そのインターフェースを用いてシーケンス制御の実装を進めました。

以下、そのときに実装した Promise like なインターフェースのコード(実際は、そのときに書いたコードを元に更に修正した)と、それを使用したサンプルコードを雑に残します。

参考

コード

基本的には Rx と似ていて、何らかのデータがどのようにして伝達していってその結果をどのように扱うか、のデータフローを先にコードで表現し、実行時に何らかのデータがそのデータフローに則って実行結果を取得します。

// sample
ExecutorService executor = Executors.newCachedThreadPool();

Promise
  .when(executor, Promise.single(new FooTask()))
  .then(new FulfillCallbackThenAll<String>() {
    @Override
    public PromiseTask.All onFulfilled(String value) {
      return Promise.all(new BarTask(), new BazTask());
    }
  })
  .then(new FulfillCallbackThenSingle<Object[], Void>() {
    @Override
    public PromiseTask.Single<Void> onFulfilled(Object[] value) {
      return Promise.single(new QuxTask());
    }
  }, new RejectCallbackDone<Throwable[]>() {
    @Override
    public void onRejected(Throwable[] value) {
      Toast.makeText(SimpleActivity.this, "failure", Toast.LENGTH_SHORT).show();
      progress.setVisibility(View.GONE);
    }
  }).atMain()
  .done(new FulfillCallbackDone<Void>() {
    @Override
    public void onFulfilled(Void value) {
      progress.setVisibility(View.GONE);
      updateViewWhenSuccess();
    }
  }, new RejectCallbackDone<Throwable>() {
    @Override
    public void onRejected(Throwable value) {
      progress.setVisibility(View.GONE);
      updateViewWhenFailure();
    }
  }).atMain();


// with lambda
Promise
  .when(executor, Promise.single(new FooTask()))
  .then((FulfillCallbackThenAll<String>) value -> Promise.all(new BarTask(), new BazTask()))
  .then(
    (FulfillCallbackThenSingle<Object[], Void>) value -> {
      return Promise.single(new QuxTask());
    },
    (RejectCallbackDone<Throwable[]>) value -> {
      Toast.makeText(SimpleActivity.this, "failure", Toast.LENGTH_SHORT).show();
      progress.setVisibility(View.GONE);
    }).atMain()
  .done(
    value -> {
      progress.setVisibility(View.GONE);
      updateViewWhenSuccess();
    },
    value -> {
      progress.setVisibility(View.GONE);
      updateViewWhenFailure();
    }).atMain();

非同期処理は Callable もしくは Runnable インターフェースで実装し、 Promise オブジェクトに渡します。

Promise.when() を呼び出して Promise オブジェクトを取得し、 Promise.then() でチェーンしつつ次の Promise オブジェクトを取得し、最後に Promise.done() を呼び出して最初の Promise オブジェクトの非同期処理を実行します。

チェーンする場合、 Promise.then() に渡すコールバックインターフェースの返り値として、次に実行したい非同期処理を表す Callable もしくは Runnable インターフェースオブジェクトを渡すことで、その次の Promise.then() に渡したコールバックインターフェースが呼ばれます。

Promise.single()/all()/race()

Callable もしくは Runnable を Promise オブジェクトに渡す際に、実行方法を指定します。

  • Promise.single(Callable/Runnable)
    • 1 つの非同期処理を実行する
  • Promise.all(Callable[]/Runnable[])
    • 全て成功した場合のみ成功時コールバックインターフェースを呼ぶ
    • どれか 1 つでも失敗した場合には失敗時コールバックインターフェースを呼ぶ
  • Promise.race(Callable[]/Runnable[])
    • どれか 1 つの非同期処理が成功/失敗に関係なく終了した時点で、成功時/失敗時コールバックインターフェースを呼ぶ

Promise.atMain()/at(Handler)

成功時/失敗時コールバックインターフェースの呼び出されるスレッドを指定します。 Promise オブジェクトごとに指定するので、 View 更新等、ステップごとに細かく制御が可能。

雑感

余程のことがないかぎりは、世に存在する便利なライブラリを使うべきなのは言うまでもなく、しかし、個人的にはまあまあ楽しめたとしとこうというのが本音だった。


Photo by Jamison McAndie | Unsplash