Bolts-Androidで連続したHTTPリクエストをウォーターフォールにする

Facebook (Parse) が開発している Bolts-Androidasyn.js のように、ネストしたコールバック呼び出しをウォーターフォールで記述できるライブラリである。

コールバックのネストをウォーターフォールで記述できるというのもありがたいが、処理をTaskに分割することで、非同期リクエストを組み合わせて使うことも簡単になっている。

Boltsを使わない、ネストした一連のHTTPリクエストは以下のようになる。ここではandroid-async-httpを使った。

https://github.com/gfx/Android-BoltsExample/blob/master/app/src/main/java/com/github/gfx/boltsexample/app/MainActivity.java

private void nestedRequests() {
  final TextView textView = (TextView) findViewById(R.id.body);

  textView.append("nested\n");

  final long t0 = System.currentTimeMillis();
  client.get(kApiBase, new RequestParams("q", "apple"), new AsyncHttpResponseHandler() {
    @Override
    public void onSuccess(int statusCode, Header[] headers, byte[] content) {
      assert statusCode == 200; // skip error handling

      textView.append("apple : " + new String(content) + "\n");

      client.get(kApiBase, new RequestParams("q", "banana"), new AsyncHttpResponseHandler() {
        @Override
        public void onSuccess(int statusCode, Header[] headers, byte[] content) {
          assert statusCode == 200; // skip error handling

          textView.append("banana : " + new String(content) + "\n");

          client.get(kApiBase, new RequestParams("q", "beef"), new AsyncHttpResponseHandler() {
            @Override
            public void onSuccess(int statusCode, Header[] headers, byte[] content) {
              assert statusCode == 200; // skip error handling

              textView.append("beef : " + new String(content) + "\n");

              client.get(kApiBase, new RequestParams("q", "xxx"), new AsyncHttpResponseHandler() {
                @Override
                public void onSuccess(int statusCode, Header[] headers, byte[] content) {
                  assert statusCode == 200; // skip error handling

                  textView.append("xxx : " + new String(content) + "\n");
                  textView.append("elapsed (nested) : " + (System.currentTimeMillis() - t0) + "ms\n");

                  waterfallRequests();
                }
              });
            }
          });
        }
      });
    }
  });
}

これはREST APIを使うAndroidアプリだと決して誇張ではない。ネストそのものはメソッドを分割することで減らすことができるが、それだと呼び出し順が固定されるので柔軟性に乏しく本質的な改善とはいえない。

これが、Boltsをつかうと以下のようになる。まず、非同期リクエストを呼んで Task を生成するメソッドをつくる。この Task は Future と概念的には同じで、生成された当初は空で非同期処理が終わるって中身がセットされると状態が変化するコンテナのようなオブジェクトである。このTaskを continueWith() / continueWithTask() でつなげて実行すれば、非同期リクエストをウォーターフォールで実行できるというわけだ。

private Task<String> getApiAsync(String word) {
  final Task<String>.TaskCompletionSource taskSource = Task.create();

  RequestParams params = new RequestParams();
  params.put("q", word);

  client.get(this, kApiBase, params, new AsyncHttpResponseHandler() {
    @Override
    public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
      String s = new String(responseBody);
      if (statusCode == 200) {
        taskSource.setResult(s);
      } else {
        taskSource.setError(new HttpResponseException(statusCode, s));
      }
    }
  });

  return taskSource.getTask();
}

private void waterfallRequests() {
  final TextView textView = (TextView) findViewById(R.id.body);
  textView.append("waterfall\n");

  final long t0 = System.currentTimeMillis();
  getApiAsync("apple").continueWithTask(new Continuation<String, Task<String>>() {
    @Override
    public Task<String> then(Task<String> task) throws Exception {
      textView.append("apple : " + task.getResult() + "\n");

      return getApiAsync("banana");
    }
  }).continueWithTask(new Continuation<String, Task<String>>() {
    @Override
    public Task<String> then(Task<String> task) throws Exception {
      textView.append("banana : " + task.getResult() + "\n");

      return getApiAsync("beef");
    }
  }).continueWithTask(new Continuation<String, Task<String>>() {
    @Override
    public Task<String> then(Task<String> task) throws Exception {
      textView.append("beef : " + task.getResult() + "\n");

      return getApiAsync("xxx");
    }
  }).continueWith(new Continuation<String, Void>() {
    @Override
    public Void then(Task<String> task) throws Exception {
      textView.append("xxx : " + task.getResult() + "\n");

      textView.append("elapsed (serial): " + (System.currentTimeMillis() - t0) + "ms\n");

      return null;
    }
  });
}

これなら見通しがいいし、順番を入れ替えることも簡単にできる。