Firebase の Task マスターになろう(パート 4: マルチタスクをマスターする)
2016年10月19日水曜日
Doug Stevenson
Developer AdvocatePlay Services Task API とその Firebase での活用についてのブログシリーズも今回が最後の投稿になります。まだ過去の投稿(パート1 2 3) を読んでいない方は、最初にそちらを読むことをご検討ください。皆さんが追いついたところで、シリーズ最終回を初めてゆきましょう。
処理が Task でない場合
このシリーズでは、今まで Task または Continuation として表される処理ユニットのみを取り扱ってきました。しかし実際は、処理を行う方法は他にもたくさんあります。さまざまなユーティリティやライブラリは、それぞれ独自の方法でマルチスレッドで処理を実行しているかもしれません。Firebase に切り替えたい場合、それらすべてを結合するために Task API に切り替えなければならないのかどうかを疑問に思う方もいるかもしれません。もちろん、そのような必要はありません。Task API は、マルチスレッドで処理を行う他の方法と繋げることができるようにデザインされています。
たとえば Java には、簡単に新しいスレッドを立ち上げて他のスレッドと並行して処理を行う機能があるため、次のようなコードを書くことができます(ただし、Android ではこのようなことは行わないことを心よりお勧めします)。
new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; // 結果で何をするか考えましょう… } }).start();
ここでは、メインスレッドから目的の String を導き出す処理を行う新しいスレッドを作成しています。その文字列を作成するためのすべての処理は、メインスレッドと並行に行われます。メインスレッドは、別のスレッドが開始されてからも実行され続けます。どこかでスレッド化された作業がブロックされた場合でも、それによってメインスレッドが停止することはありません。しかし、その String の結果を必要な場所に配置するために、何かを行う必要があります。Android では、メインスレッドに戻る必要がある場合、そうするためのコードをもう少し書く必要があります。それによって、少しばかりコードが見にくくなるかもしれません。それを避けるために、Task を使うことができます。
Play Services Task API には、他の処理ユニットを Task のように動作させる方法があります。そのユニットが Task を意図した実装になっていなくても構いません。ここで注目すべきクラスは、TaskCompletionSource です。このクラスを使うと、成功か失敗を判定するコードを少しばかり追加して Task の「プレースホルダ」を効率的に作成できます。先ほどのスレッドを Task API で実装しなくても Task のような振る舞いをさせたい場合、(前回、Callable を Tasks.call() に渡す方法を学習しました)次のようにすることができます。
final TaskCompletionSource<String> source = new TaskCompletionSource<>(); new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; source.setResult(result); } }).start(); Task<String> task = source.getTask(); task.addOnCompleteListener(new OnCompleteListener<String>() { ... });
こうすることによって、setResult() メソッドを使って結果の String を TaskCompletionSource に提供するスレッドを作ることができます。その後、呼び出し元のスレッド内で、TaskCompletionSource から「プレースホルダ」となる Task を取得し、それにリスナーを追加することができます。こうすることでスレッドの結果は、メインスレッドで実行されているリスナーの内部で処理されます。TaskCompletionSource の setException() メソッドを呼ぶと、失敗したケースにも同じように対応できます。その場合、最終的に失敗を検知するリスナーが実行されることになり、そのリスナーが例外を検知します。
この戦略は少しばかりおかしく感じられるかもしれません。作業の結果はもう少し簡単な方法を使ってもメインスレッドに渡すことができるからです。この方法が貴重なのは、新しいプレースホルダの Task と、他で使用している Task を統合的に扱える点です。
すべて言い終わり、やり終わったら・・・
Firebase Realtime Database 内にある値と Firebase Remote Config の値に完全に依存するアプリを書くことを想像してみてください。その際に、データの読み込みを待つ間にもユーザーに楽しんでもらえるよう、データが使えるようになるまで、アニメーションするスプラッシュ画面を作りたいものとします。さらに、データがローカルにキャッシュされていた場合、画面が出たり消えたりしてちらつかないように、最低 2 秒待ってからその画面を表示するものとします。あなたなら、この画面をどのように実装するでしょうか?
初心者の方であれば、新しい Activity を作ってスプラッシュ画面用のビューをデザインし、実装しなければならないでしょう。これは単純な方法です。次に、Realtime Database と Remote Config に対する処理と、2 秒のタイマーという要素を協調させることになります。そして、おそらくスプラッシュ画面のビューを作成した後で、それらすべての処理を Activity の onCreate() の中で起動することになるでしょう。一連の Continuation を使って、すべての処理が 1 つずつ順番に行われるようにすることもできます。しかし、そうする代わりに、処理をすべて同時に開始して並列に実行し、最も長い時間がかかっている処理が終わるまでユーザーを待たせることができたらどうでしょう。これをどう実現できるかを見てみましょう。
Task API には、複数の Task がすべて完了したときに知らせてくれるいくつかのメソッドがあります。次の static ユーティリティ メソッドは、提供した Task のコレクションが完了した際に反応してトリガーされる新しい Task を生成します。
Task<Void> Tasks.whenAll(Collection<? extends Task<?>> tasks) Task<Void> Tasks.whenAll(Task...<?> tasks)
whenAll() の 1 つは、Java コレクション(List または Set)を受け取ります。もう 1 つは、可変個引数関数方式を使用して任意の長さの配列を簡単に作成できるようになっています。どちらの場合でも、返される Task は、すべての Task が成功した場合に成功を、いずれか 1 つでも失敗した場合には失敗をトリガーします。新しい Task の結果のパラメータは Void となっており、直接的には何の結果も含んでいないことに注意してください。個別の Task の結果が必要な場合は、それぞれの Task から直接取り出す必要があります。
この whenAll() 関数は、すべての処理を同時に実行し、すべて終了した際に知りたい場合、とても便利です。これによって、ユーザーをスプラッシュ画面の先に進ませることができます。ここでポイントとなるのは、待機する個々の項目を表す一連の Task オブジェクトをどうにかして取得することです。
Remote Config のタスク化
Remote Config の fetch は Task を返してくれるので簡単です。これをリッスンすると、値が使えるようになったことがわかります。タスクを起動して、そのタスクを覚えておきましょう。
private Task<Void> fetchTask; // onCreate の中で: fetchTask = FirebaseRemoteConfig.getInstance().fetch();
Realtime Database のタスク化
Realtime Database は Task を提供していません。そのため、完了時にトリガーされてデータが使えるようになったことを通知してくれることはなく、簡単にはいきません。しかし、先ほど学習したばかりの TaskCompletionSource を使って、データが利用できるようになったときにプレースホルダ タスクをトリガーすることができます。
private TaskCompletionSource<DataSnapshot> dbSource = new TaskCompletionSource<>(); private TaskdbTask = dbSource.getTask(); // onCreate の中で: DatabaseReference ref = FirebaseDatabase.getInstance().getReference("/data/of/interest"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { dbSource.setResult(dataSnapshot); } @Override public void onCancelled(DatabaseError databaseError) { dbSource.setException(databaseError.toException()); } });
ここでは、アプリの起動を継続するために必要なデータのリスナーを登録しています。次に、このリスナーは、受信したコールバックに基づいて dbSource 経由で dbTask の成功または失敗をトリガーします。
遅延のタスク化
最後は遅延です。スプラッシュ画面を表示する前には、最低 2 秒間待たなければなりません。この遅延も、TaskCompletionSource を使って Task として表現することができます。
private TaskCompletionSource<Void> delaySource = new TaskCompletionSource<>(); private Task<Void> delayTask = delaySource.getTask(); // onCreate の中で: new Handler().postDelayed(new Runnable() { @Override public void run() { delaySource.setResult(null); } }, 2000);
この遅延は、2,000 ミリ秒後にメインスレッドで実行される Runnable をスケジュールするだけで実現できます。この Runnable は、delaySource 経由で delayTask をトリガーします。
以上で、すべて並列に処理される 3 つの Task がそろいました。Tasks.whenAll() を使ってすべて成功した際にトリガーされる別の Task を作ることができます。
private Task<Void> allTask; // onCreate() の中で: allTask = Tasks.whenAll(fetchTask, dbTask, delayTask); allTask.addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { DataSnapshot data = dbTask.getResult(); // DB のデータに対する処理 startActivity(new Intent(SplashScreenActivity.this, MainActivity.class)); } }); allTask.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // ユーザーに十分謝る! } });
これでうまく動くはずです。最後の allTask が成功すると、Database からのデータを使って必要なことを何でも行うことができます。その後、ユーザーを MainActivity に送ります。ここで Task を使わないと、このコードはもっと長くて書くのが大変なものになります。それぞれの処理が終わった際に、すべての他の継続している処理ユニットの状態をチェックして、すべてが完了している場合のみ先に進む必要があるからです。ここでは、Task API がそういった細かいことを肩代わりしてくれます。さらに、必要に応じてさらに多くの Task を簡単に追加することもでき、その場合でもロジックを変える必要はありません。ただ単に allTask の背後にあるコレクションに Task を追加すればいいだけです。
最後に一言
1 つまたは複数の Task の結果に従って現在のスレッドをブロックする方法があることを知っておくとよいでしょう。通常、必然がない場合はスレッドをブロックしたいとは思わないでしょうが、必然がある場合には便利です(Loader で使う場合など)。Task の結果に従って待機する必要がある場合、await() 関数を使います。
static <TResult> TResult await(Task<TResult> task) static <TResult> TResult await(Task<TResult> task, long timeout, TimeUnit unit)
await() を使うと、呼び出し元スレッドは単純にタスクが完了するか、指定されたタイムアウト時間が経過するまでブロックされます。成功した場合は結果オブジェクトを受け取りますが、失敗した場合は ExecutionException がスローされます。この例外には、根本原因がラップされています。メインスレッドは絶対にブロックしてはいけないことを覚えておいてください。この方法は、何らかのバックグラウンド スレッドを動かしていることを知っているときにのみ使うようにします。
まとめ(シリーズ全体)
このブログシリーズで取り扱ってきた 4 つのパートは、以下のとおりです。
- パート 1: いくつかの Firebase API は、完了時にリスナーに通知する Task を返します。
- パート 2: Task API は、Task をリッスンする豊富なオプションを提供します。
- パート 3: Task API は、独自の Task を作成する方法を提供し、結果に対する操作のチェーンを実行します。
- パート 4: あらゆる非同期処理は Task に変換することができます。また、単一の完了点を持つ複数の Task を並列に実行することもできます。
Play Services Task API を効率的に使用するために知っておくべきことは、これがすべてです。皆様が Firebase と Task API を使いこなし、効率的で快適な Android アプリを作ることを願っています。
Posted by Khanh LeViet - Developer Relations Team