Doug Stevenson
Developer Advocate

Play 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 Task dbTask = 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



Doug Stevenson
Developer Advocate

どうも。Play サービスの Task API for Android について取り上げているブログシリーズのパート 3 にようこそ。パート 1 では API の基本を、パート 2 では最適なリスナーのスタイルを選択する方法を見てきました。つまり、皆さんは既に Firebase API が生成するタスクを効率的に使用できるようになっています。しかし、タスクの高度な使用方法についてさらに踏み込んでみたい方は、ぜひ本記事をお読みください。

Task に取りかかる
Android 向けの Firebase 機能には、タスクが完了した際に通知してくれるものがあります。しかし、スレッドで動作する独自タスクを作成したい場合はどうすればいいでしょうか。Task API は、こういった場合に役立つツールです。アプリに Firebase を組み込まずに Task API を使いたい場合は、build.gradle に次の依存性を追加すると、必要なライブラリを取得できます。
    compile 'com.google.android.gms:play-services-tasks:9.6.1'

しかし、Firebase を 組み込む 場合は、このライブラリが自動的に取得されるため、個別に指定する必要はありません。

新しい Task の起動に使うメソッドは 1 つだけですが、呼び出しの方法は 2 通りあります。これには、Task ユーティリティ クラスの「call」という静的メソッドを使います。2 つの呼び出し方法を次に示します。
    Task<TResult> call(Callable<TResult> callable)
    Task<TResult> call(Executor executor, Callable<TResult> callable)

addOnSuccessListener() と同様に、call() にもメインスレッド上で実行するものと、作業を Executor に送信するものがあります。実行する作業は、渡される Callable の中で指定します。Java の Callable は Runnable に似ていますが、結果の型パラメータを持っており、その型が call() メソッドが返すオブジェクトの型になります。この結果の型は、call() が返す Task の型にもなります。次に示すのは、String を返すだけの単純な Callable です。
    public class CarlyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "Call me maybe";
        }
    }

CarlyCallable のパラメータは String であることに注目してください。これは、call() メソッドが String を返す必要があることを示しています。ここから Task を作成する処理は 1 行で書くことができます。
    Task<String> task = Tasks.call(new CarlyCallable());

この行が実行されると、CarlyCallable の call() メソッドがメインスレッドで呼び出され、(結果はわかりきっていますが)Task にリスナーを追加して結果を求めることができます。さらに興味深い Callable は、実際にデータベースやネットワーク エンドポイントからデータを読み込むものでしょう。そのような重い処理を伴う Callable は、最初の引数として Executor を渡すことができる call() の 2 番目の形式を使って Executor 上で実行することができます。

複数の Task を繋げる
例として、CarlyCallable の Task の結果として生成された String を処理することを考えてみましょう。ここでは、結果の String の文字列自体に興味があるのではなく、文字列を個々の単語に分割し List<String> に格納されたものに興味があるとします。しかし、CarlyCallable を変更する必要があるとは限りません。CarlyCallable は想定される作業を正確に行っており、別の場所でこのまま利用できるからです。そのため、CarlyCallable が返した String をそれぞれの単語に分割するロジックを作成し、CarlyCallable の処理の後に実行したいです。これは、Continuation を使って実現できます。Continuation インターフェースの実装は 1 つの Task の出力を受け取り、ある処理を行って、結果のオブジェクトを返します。これは同じ型でなくても構いません。次に示すのは、文字列を各単語ごとに String の List に分割する Continuation です。
    public class SeparateWays implements Continuation<String, List<String>> {
        @Override
        public List<String> then(Task<String> task) throws Exception {
            return Arrays.asList(task.getResult().split(" +"));
        }
    }

ここで実装されている Continuation インターフェースには、入力の型(String)と出力の型(List)の 2 つの型パラメータがあることに注目してください。入力と出力の型は、インターフェースの唯一のメソッドである then() のシグネチャに使用されており、このメソッドで行われる操作を定義しています。特に注目する必要があるのは、then() に渡されるパラメータの Task です。この String は、Continuation インターフェースの入力型と一致している必要があります。そうすることによって Continuation は入力を得ることができ、完了した Task から結果を引き出すことができます。

次に示すのは、String の List を無作為に並べ変える別の Continuation です。
    public class AllShookUp implements Continuation<List<String>, List<String>> {
        @Override
        public List<String> then(@NonNull Task<List<String>> task) throws Exception {
            // Randomize a copy of the List, not the input List itself, since it could be immutable
            final ArrayList<String> shookUp = new ArrayList<>(task.getResult());
            Collections.shuffle(shookUp);
            return shookUp;
        }
    }

さらに、String の List を 1 個のスペースで区切った String に結合するものも作ります。
    private static class ComeTogether implements Continuation<List<String>, String> {
        @Override
        public String then(@NonNull Task<List<String>> task) throws Exception {
            StringBuilder sb = new StringBuilder();
            for (String word : task.getResult()) {
                if (sb.length() > 0) {
                    sb.append(' ');
                }
                sb.append(word);
            }
            return sb.toString();
        }
    }

何をしようとしているのかはもうお分かりかもしれません。以上の操作をまとめて、最初の Task の String の単語の順番を無作為化し、その結果から新しい String を生成する操作のチェーンにします。
    Task<String> playlist = Tasks.call(new CarlyCallable())
            .continueWith(new SeparateWays())
            .continueWith(new AllShookUp())
            .continueWith(new ComeTogether());
    playlist.addOnSuccessListener(new OnSuccessListener<String>() {
        @Override
        public void onSuccess(String message) {
            // The final String with all the words randomized is here
        }
    });

Task の continueWith() メソッドは、指定された Continuation によって処理された前の Task に対する計算を行うことを示す新しい Task を返します。ここで行っているのは、continueWith() の呼び出しをチェーンさせることです。そのようにして操作のパイプラインを形成し、各ステージの完了を待ってから次のステージを実行するような合成 Task を作成できます。

この操作のチェーンは、巨大な String を扱う場合に問題になるかもしれません。そこで、すべての処理を別のスレッドで行い、メインスレッドをブロックしないように変更してみましょう。
    Executor executor = ... // you decide!

    Task<String> playlist = Tasks.call(executor, new CarlyCallable())
        .continueWith(executor, new SeparateWays())
        .continueWith(executor, new AllShookUp())
        .continueWith(executor, new ComeTogether());
    playlist.addOnSuccessListener(executor, new OnSuccessListener() {
        @Override
        public void onSuccess(String message) {
            // Do something with the output of this playlist!
        }
    });

これで、Callable とすべての Continuation と最後の Task リスナーは、 すべて Executor が決めるスレッド上で実行されるようになり、処理の間にメインスレッドが UI などの処理を行えるようになります。これで、UI がもたつくことはなくなります。

初めて見たときは、すべての操作を別のクラスに分けるのはどうかと思うかもしれません。必要なことだけを行う数行のメソッドを 1 つ書くだけで済ませることもできるでしょう。ただし、この例は、Task がどのように動作するかに注目した簡単な例であることにも注意してください。Task と Continuation のチェーンによるメリットは、(かなり単純な機能であるにもかかわらず)次のようなことを行う場合に顕著に表れます。
  • 入力される String を新しいソースから取得したい場合はどうしますか。たとえば、BlondieCallable やPaulSimonCallable があった場合はどうでしょう。
  • 入力される String に対して別の処理を行う場合はどうしますか。たとえば、List 内の String の順番を(レコードのように)右方向に回転させる YouSpinMeRound という Continuation を行う場合です。
  • 別のスレッドで処理パイプラインの別のコンポーネントを実行したい場合はどうしますか。

現実的に、Task の Continuation は、データセットに対する filter、map、reduce 機能の一連のモジュール チェーンを実行するために使うことが多いでしょう。また、コレクションが大きくなる可能性がある場合は、こういった動作をメインスレッド以外で行いたい場合もあります。しかし、ここで音楽を楽しむこともできるでしょう!

Task のチェーンの実行中でエラーが発生したら
最後に、Continuation についてもう 1 つ知っておくべきことがあります。どこかのステージの処理中にランタイム例外がスローされた場合、通常、その例外はチェーンの最後の Task の失敗を監視するリスナーまで伝播されます。任意の Continuation では、入力 Task に対して isSuccessful() メソッドを実行し、タスクが成功したかどうかを確認できます。または、(上記の例で行っているように)単に getResult() を呼び出すと、以前に失敗があれば例外が再スローされ、自動的に次の Continuation にたどり着きます。ただし、失敗する可能性がある場合、チェーンの最後の Task のリスナーで必ず失敗を確認するようにします。

たとえば、上記のチェーンの CarlyCallable が null を返した場合、SeparateWays を行う Continuation が NullPointerException をスローし、それが最後の Task まで伝播します。Task に OnFailureListener が登録されている場合、同じ例外のインスタンスによってそれが呼び出されます。

ポップクイズ
先ほどのチェーンで、処理コンポーネントを変更せずに元の文字列に含まれる単語の数を見つけ出す 最も効率的 な方法は何でしょうか。続きを読む前に、少し考えてみてください。

答えは、おそらく皆さんが考えるよりもシンプルなものです。並び順が無作為になっているだけなので、最もわかりやすいソリューションは、最終的な出力文字列の単語の数を数えることです。しかし、そこにもう 1 つのトリックが潜んでいます。continueWith() が呼ばれるたびに新しい Task インスタンスが返されますが、それらはここで参照することができません。なぜなら、チェーン構文を利用して最後の Task に連結されているためです。そのため、任意のタスクをインターセプトして、次の Continuation に加えて、別のリスナーを追加するという方法をとります。
    Task<List<String>> split_task = Tasks.call(new CarlyCallable())
        .continueWith(executor, new SeparateWays());
    split_task =
        .continueWith(executor, new AllShookUp())
        .continueWith(executor, new ComeTogether());
    split_task.addOnCompleteListener(executor, new OnCompleteListener<List<String>>() {
        @Override
        public void onComplete(@NonNull Task<List<String>> task) {
            // Find the number of words just by checking the size of the List
            int size = task.getResult().size();
        }
    });
    playlist.addOnCompleteListener( /* as before... */ );

Task が完了すると、 両方の Continuation が実行され、それとともに すべての 追加されたリスナーが実行されます。ここで行ったのは、SeparateWays という Continuation の出力を取得する Task をインターセプトし、その出力を直接リッスンする処理を追加しただけです。Continuation のチェーンには影響しません。インターセプトしたタスクで List に対して size() を呼び出すだけで、単語の数を取得できます。

このシリーズのパート 3 のまとめ
いろいろな冗談はさておき、Task API を使用すると、シーケンシャルなパイプライン処理を簡単にモジュール形式で表現して実行することができます。さらに、処理の各ステージでどの Executor を使うかも指定できます。この処理は、Firebase をアプリに組み込むか組み込まないかにかかわらず、独自の Task や Firebase API の Task を利用して実行できます。本シリーズの最終回となる次回では、Task を使って複数の作業を同時実行する並列処理の起動方法について見ていきます。

いつものように、質問がありましたら、Twitter でハッシュタグ #AskFirebase を検索するか、firebase-talk Google Group をご利用ください。専用の Firebase Slack チャネルも用意しています。Twitter で @CodingDoug のフォローもよろしくお願いします。シリーズの次の記事が投稿された際にお知らせいたします。

最後に、この投稿に登場したすべての曲を上げておきましょう。


Posted by Khanh LeViet - Developer Relations Team



Doug Stevenson
デベロッパー アドボケート

こんにちは。Play Services Task API についてのブログシリーズのパート 2 をお届けします。Firebase API では、一部の Firebase 機能の API が非同期に実行する操作に応答する方法として、Play Services Task API を利用しています。前回は、Firebase Storage API のタスクを紹介するとともに、タスクが動作する一般的な仕組みの一部を学びました。もし前回の投稿をご覧になっていない場合は、以降を読む前にご覧いただくことをお勧めします。今回の投稿では、結果を取得するためにタスクに追加できるさまざまなリスナーの動作の違いについて見ていきます。

前回は、Firebase Storage API を使う次のようなタスクにリスナーを追加しました。
    Task task = forestRef.getMetadata();
    task.addOnSuccessListener(new OnSuccessListener() {
        @Override
        public void onSuccess(StorageMetadata storageMetadata) {
            // Metadata now contains the metadata for 'images/forest.jpg'
        }
    });

このコードでは、完了時に起動される匿名リスナーを単一の引数として addOnSuccessListener() を呼び出しています。この形式を使うと、リスナーはメインスレッドで起動します。つまり、ビューの更新など、メインスレッドでしかできないことを行うことができます。タスクによって制御がメインスレッドに戻されるのはたいへんありがたいことです。ただし、1 点だけ注意事項があります。アクティビティでこのようにしてリスナーを登録した場合、そのアクティビティが破棄されるときにリスナーを削除しないと、アクティビティがリークする可能性があります。

アクティビティのリークを防止する

もちろん、アクティビティのリークを発生させたい人はいません。そのためにまず、アクティビティのリークとはどのようなものか考えてみましょう。簡潔に言えば、アクティビティのリークは、アクティビティの onDestroy() ライフサイクル メソッドが呼び出された後(つまり、アクティビティが利用できる期間を超えた後)もアクティビティ オブジェクトの参照を保持しているオブジェクトがある場合に発生します。アクティビティで onDestroy() が呼び出されると、Android はそのアクティビティのインスタンスをもう利用しないことが確実にわかります。onDestroy() が呼ばれた後、Android ランタイムのガベージ コレクタはそのアクティビティ、付随するすべてのビュー、その他の不要になったオブジェクトを回収します。しかし、別のオブジェクトがアクティビティに対する強い参照を保持している場合、ガベージ コレクタはそのアクティビティやそれに付随するすべてのビューを回収しません。

タスクを使うと、慎重に回避しない限り、アクティビティのリークが問題になる場合があります。(アクティビティの内部にある場合)先ほどのコードの非同期リスナー オブジェクトは、自身を含むアクティビティへの暗黙的な強い参照を保持します。リスナー内部のコードがアクティビティやそのメンバーを変更できるのはそのためですが、このあたりの細かい制御はコンパイラが暗黙的に行ってくれます。アクティビティのリークは、実行中のタスクがアクティビティの onDestroy() が呼び出された後もリスナーを参照している場合に発生します。しかし実際は、タスクにどのくらい時間がかかるかについては何の保証もありません。そのため、リスナーは無限に保持できるようになっています。また、リスナーは暗黙的にアクティビティへの参照を保持しているため、タスクが onDestroy() の前に完了していないとアクティビティがリークする可能性があります。(たとえば、ネットワーク通信が滞っている場合など)たくさんのタスクがアクティビティへの逆参照を保持している場合、アプリのメモリが足りなくなってクラッシュすることもあります。そうなっては一大事です。詳しくは、こちらの動画もご覧ください。

手元のタスクに戻る

単一引数版の addOnSuccessListener() を使うと、適切なタイミングでリスナーを削除するよう十分注意しない限り、アクティビティがリークする可能性があります。アクティビティのリークを考慮する場合(もちろん考慮が必要です)、この点を認識しておく必要があります。

Task API を使うと、リスナーの削除を自動的に行ってくれるので便利です。先ほどのコードがアクティビティの中にあるものとして、addOnSuccessListener() を呼び出す部分を少し変更してみましょう。
    Task task = forestRef.getMetadata();
    task.addOnSuccessListener(this, new OnSuccessListener() {
        @Override
        public void onSuccess(StorageMetadata storageMetadata) {
            // Metadata now contains the metadata for 'images/forest.jpg'
        }
    });

これは、addOnSuccessListener() に 2 つの引数があることを除けば、先ほどのものとまったく同じです。最初の引数は「this」です。そのため、このコードがアクティビティの中にある場合、「this」はアクティビティのインスタンスを指します。最初のパラメータにアクティビティへの参照を指定すると、そのアクティビティのライフサイクルのみを「スコープ」としたリスナーであることを Task API に伝えることができます。こうすることによって、アクティビティの onStop() ライフサイクル メソッドが呼び出された際にリスナーが 自動的に タスクから削除されるようになります。アクティビティがアクティブである間に作成したすべてのタスクを自分で削除する必要がなくなるため、これはとても便利な方法です。リスナーを停止する場所が、アクティビティが見えなくなった際に呼び出される onStop() で適切かどうかを確認する必要はありますが、多くの場合は問題ないでしょう。なお、次のアクティビティ(画面の向きが変わって現在のアクティビティが新しいアクティビティに置き換わった場合など)でタスクのトラッキングを継続する場合、次のアクティビティで必要になる情報を保持する方法を検討する必要があります。この点については、アクティビティの状態を保存するをご覧ください。

メインスレッド以外で処理する

タスクが完了した際の処理をメインスレッド上で行いたくないケースもあるでしょう。リスナーでブロック処理を行いたい場合や、それぞれのタスクの結果を(シーケンシャルではなく)並列に処理したい場合などです。その場合、メインスレッドでの処理ではなく、代わりに自分で制御する別のスレッドで結果を処理したいと思うでしょう。addOnSuccessListener() には、アプリのスレッド化に便利なもう 1 つの形式があります。次のような形式です(リスナーは省略しています)。
    Executor executor = ...;  // obtain some Executor instance
    Task task = RemoteConfig.getInstance().fetch();
    task.addOnSuccessListener(executor, new OnSuccessListener() { ... });


ここでは、Firebase Remote Config API を呼び出して新しい設定値を取得しています。次に、fetch() から返されたタスクに対して addOnSuccessListener() を呼び出し、最初の引数としてエグゼキュータを渡します。エグゼキュータは、リスナーを起動するスレッドを決定します。エグゼキュータについて詳しくない方のために説明すると、エグゼキュータは作業単位を受け取り、自らの制御下にあるスレッドで実行するためにルーティングを行うコア Java ユーティリティです。制御下にあるスレッドとは、単一スレッドの場合もありますが、作業を行うべく待ち構えているスレッドプールである場合もあります。アプリで直接エグゼキュータを使うことはあまり多くはなく、この手法はアプリのスレッドの動作を管理する高度な技法と見られることもあります。ここで覚えておくべきことは、状況によっては、リスナーの結果を受信するのはメインスレッドでなくてもよいということです。エグゼキュータを使用する場合は、スレッドがリークしないように共有シングルトンとして管理するか、エグゼキュータのライフサイクルを管理するようにします。

このコードで特筆しておくべきもう 1 つの興味深い点は、Remote Config から返されるタスクのパラメータが Void になっていることです。こうすることによって、タスクがオブジェクトを直接生成しないことを宣言できます。Java では、Void は型データがないことを示すデータ型です。Remote Config API は、このタスクを単なるタスクの完了を示すインジケーターとして使用しています。呼び出し元は、取得された新しい値を検出するために、別の Remote Config API を使用して頂く必要があります。

賢い選択を

まとめると、addOnSuccessListener() には次の 3 つの形式があります。
    Task addOnSuccessListener(OnCompleteListener listener) 
    Task addOnSuccessListener(Activity activity, OnSuccessListener listener) 
    Task addOnSuccessListener(Executor executor, OnSuccessListener listener) 

さらに、失敗時と完了時のリスナーにも同じ形式があります。
    Task addOnFailureListener(OnFailureListener listener)
    Task addOnFailureListener(Activity activity, OnFailureListener listener)
    Task addOnFailureListener(Executor executor, OnFailureListener listener)

    Task addOnCompleteListener(OnCompleteListener listener)
    Task addOnCompleteListener(Activity activity, OnCompleteListener listener)
    Task addOnCompleteListener(Executor executor, OnCompleteListener listener)

OnCompleteListener とは


OnCompleteListener には、特別なことは何もありません。1 つだけで成功と失敗の両方を受信できるリスナーなので、コールバックの内部でステータスを確認する必要があります。前回の投稿のファイル メタデータのコールバックは、成功と失敗でリスナーを分けずに、次のように書き換えることもできます。
    Task task = forestRef.getMetadata();
    task.addOnCompleteListener(new OnCompleteListener() {
        @Override
        public void onComplete (Task task) {
            if (task.isSuccessful()) {
                StorageMetadata meta = task.getResult();
                // Do something with metadata...
            } else {
                Exception e = task.getException();
                // Handle the failure...
            }
        }
    });


つまり、OnCompleteListener を使うと 1 つのリスナーで成功と失敗の両方の処理を行うことができます。コールバックに渡される Task オブジェクトに対して isSuccessful() を呼び出すと、成功か失敗かがわかります。実質的には、OnSuccessListener と OnFailureListener の両方を登録するのと同じ機能になります。どちらの方式を使うかは、主に好みの問題です。

このシリーズのパート 2 のまとめ

タスクの結果は成功、失敗、完了という 3 種類のリスナーで受信できます。また、それぞれのリスナーは、メインスレッド、アクティビティをスコープとしたメインスレッド、エグゼキュータが決定するスレッドという 3 つの方法でコールバックを受信することができます。いくつかの選択肢がありますが、どれが一番状況に適しているかを選ぶのは皆さんです。しかし、タスクの結果を処理する方法はこれだけではありません。さらに複雑な処理を行うために、タスク結果のパイプラインを作成することもできます。次回は、その方法を詳しく説明します。Firebase Task マスターへの旅を続けましょう。

質問がありましたら、Twitter でハッシュタグ #AskFirebase を検索するか、firebase-talk Google Group をご利用ください.専用の Firebase Slack チャネルも用意しています。Twitter で @CodingDoug のフォローもよろしくお願いします。
ブログシリーズの パート 1 もお読みください。


Posted by Khanh LeViet - Developer Relations Team



Doug Stevenson
デベロッパー アドボケート


Android 向けの Firebase のクライアント API を使用する際に、デベロッパーのリクエストに応じて Firebase で非同期的に処理を実行させたいケースがあります。たとえばリクエストされたデータをすぐに取得できない場合や、一番最後に何か処理を実行しなければならない場合などです。なお、「アプリ内で処理を 非同期的に 行う」というのは、アプリにおいて最優先すべきビューのレンダリング処理と同時に実行しつつ、かつレンダリングの妨げにならないように処理をする、という意味です。この非同期処理を Android アプリで適切に行えば、任意の処理が Android のメインスレッドを長時間を占有するという事態は回避できます。逆に非同期処理に問題があると、一部のフレームのレンダリングが遅れ、ユーザー エクスペリエンスにおいてカクツキが発生します。さらにひどいと、ANR という最悪の状態に陥ります。遅延を引き起こす代表的な処理には、ネットワーク リクエスト、ファイルの読み取りおよび書き込み、時間のかかる演算などがあります。これらを総称して ブロッキング タスク と呼びます。メインスレッドのブロックは絶対に避けたいところです。

Firebase API を使用して、通常メインスレッドをブロックするような処理をリクエストする場合は、カクツキや ANR を回避するため、その処理を別スレッドで行うよう API で調整をしなければなりません。処理が完了した後は、確実にビューを更新するために結果をメインスレッドに返さなければならない場合もあります。

これを行うのが Play Services Task API です。Task API の目的は、簡単かつ軽量な、Firebase (および Play サービス)クライアント API 向けの Android 対応フレームワークを提供し、処理を非同期に実行できるようにすることです。この API は、Firebase と共に Google Play Services 9.0.0 で導入されました。アプリに Firebase の機能を使用している方は、気づかないうちに Task API を使用していたかもしれません。この一連のブログ記事では、Firebase API で Task を利用する方法を紹介し、高度な利用パターンもいくつか取り上げます。

本題に入る前に知っておいていただきたいのが、Android のあらゆるスレッド技術を Task API で代用できるわけではないという点です。スレッド用のツールには ServicesLoadersHandlers など多様なものがあり、各機能の内容は Android チームが詳細なドキュメントにまとめています。さらに YouTube には Application Performance Patterns の全シーズンがアップされており、こちらの動画でさまざまな手法を学ぶことができます。また、サードパーティのライブラリを利用して Android アプリのスレッド処理を行っているデベロッパーもいます。ぜひ、さまざまなソリューションについて学び、自身のスレッド要件に合った最適な手法を見つけてください。なお Firebase API では、スレッド処理の管理に一貫して Task を使用していますので、これと相性のよい手法を組み合わせて利用することをおすすめします。

シンプルな Task の例

Firebase Storage をご利用の場合、どこかで必ず Task を目にしたことがあるはずです。以下はファイルのメタデータのドキュメントからそのまま引用したコードで、ストレージにアップ済みのファイルのメタデータを取得するシンプルな例です。
    // Create a storage reference from our app
    StorageReference storageRef = storage.getReferenceFromUrl("gs://");

    // Get reference to the file
    StorageReference forestRef = storageRef.child("images/forest.jpg");

    forestRef.getMetadata().addOnSuccessListener(new OnSuccessListener() {
        @Override
        public void onSuccess(StorageMetadata storageMetadata) {
            // Metadata now contains the metadata for 'images/forest.jpg'
        }
    }).addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception exception) {
            // Uh-oh, an error occurred!
        }
    });

このコードには「Task」が見当たりませんが、実際には Task が機能している箇所があります。上記のコードの後半部分は、次のように書き換えることができます。
    Task task = forestRef.getMetadata();
    task.addOnSuccessListener(new OnSuccessListener() {
        @Override
        public void onSuccess(StorageMetadata storageMetadata) {
            // Metadata now contains the metadata for 'images/forest.jpg'
        }
    });
    task.addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception exception) {
            // Uh-oh, an error occurred!
        }
    });

どうやら、コード内に隠された Task があったようです。

処理をすると約束する

書き直した上記のコードを見ると、Task を使用してファイルのメタデータを取得する方法がよくわかります。StorageReference の getMetadata() メソッドは、ファイルのメタデータがすぐには利用できないと想定して、データを取得するためにネットワーク リクエストを行います。そのネットワーク アクセスにおいて呼び出し元のスレッドがブロックされないように、getMetadata() は Task を返しておきます。この Task をリッスンすることで、最終的に処理が成功したか失敗したかがわかります。次に API は、制御するスレッドでリクエストを実行するよう調整します。このスレッド処理は API 内で行われるため詳細は把握できませんが、結果が利用できるようになると、返された Task を介して通知がきます。返された Task は、処理が完了した後に、追加したリスナーのいずれかを必ず起動することを保証するものです。このような形で非同期処理の結果を扱う API は、プログラミング環境によっては Promise と呼ばれます。

ここで、返された Task が StorageMetadata 型によってパラメータ化されていることに注目してください。これは OnSuccessListener で onSuccess() に渡されるオブジェクトの型でもあります。実際、すべての Task はこのように総称型を宣言して生成したデータの型を示し、その総称型を OnSuccessListener で共有する必要があります。また、エラーが発生すると、Exception が OnFailureListener の onFailure() に渡されます。これは、失敗時に実行する指定の例外処理になります。Exception の詳細情報を知りたい場合は、その型を調べて、期待する型に安全にキャストする必要があります。

最後にもう 1 つ、このコードについて知っておくべきことは、リスナーはメインスレッドから呼ばれるということです。Task API で、自動的にそうなるように調整をしています。StorageMetadata が利用可能になった際に、メインスレッドで行わなければならない処理がある場合は、それをリスナー メソッド内で実行できます。(ただしメインスレッドのリスナー内では、ブロッキング タスクを決して実行しないように注意してください)これらのリスナーには他にもさまざまな利用方法がありますので、次回以降の記事でその詳細を説明します。

一発勝負

Firebase には、Task に関連のないリスナーに対応した API を提供する機能もあります。たとえば Firebase Authentication を使用している場合、ユーザーがアプリのログイン、ログアウトに成功した瞬間を検出するために、ほぼ間違いなくリスナーを登録しているはずです。
    private FirebaseAuth auth = FirebaseAuth.getInstance();
    private FirebaseAuth.AuthStateListener authStateListener = new FirebaseAuth.AuthStateListener() {
        @Override
        public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
             // Welcome! Or goodbye?
        }
    };

    @Override
    protected void onStart() {
        super.onStart();
        auth.addAuthStateListener(authStateListener);
    }

    @Override
    protected void onStop() {
        super.onStop();
        auth.removeAuthStateListener(authStateListener);
    }

FirebaseAuth クライアント API は、addAuthStateListener() でリスナーを追加した際に、2 つの主要な処理を必ず実行します。まずは、現在判明しているユーザーのログイン状態に基づいて、すぐにリスナーを呼び出します。もう 1 つは、リスナーが FirebaseAuth オブジェクトに追加されている限り、以降、ユーザーのログイン状態が変わるたびに毎回リスナーを呼び出します。この動作は Task とは大きく異なります。

Task の場合は、結果が利用可能になって初めて、追加されたリスナーを最大で 1 回だけ呼び出すことができます。また、リスナーが追加される前に、既に結果が利用可能であった場合は、Task はすぐにリスナーを起動します。Task オブジェクトは、最終的な結果オブジェクトを効率的に記憶しておき、その内容を以降のリスナーに引き継いで処理します。これは、リスナーがすべてなくなり、ガベージ コレクションが実行されるまで続きます。よって Task オブジェクト以外のもので、リスナーと連携する Firebase API を使用する際は、必ずその動作を理解するようにしてください。すべての Firebase リスナーが Task リスナーのように動作するわけではありません。


もう 1 つの重要なステップ

追加した Task リスナーのアクティブな有効期間を考慮しましょう。それを怠ると 2 つの問題が生じます。まず、追加したリスナーが参照しているアクティビティと、そのビューの有効期間を超えて Task が動作することで、アクティビティのリークが生じるおそれがあります。次に、不要になったリスナーが実行されると無駄な処理が走り、すでに無効なアクティビティ状態にアクセスする処理が行われる可能性があります。このブログの次のパートでは、これらの問題とその回避方法を詳しく説明します。

このシリーズのパート 1 のまとめ

今回は Firebase のサンプルコードについて簡単に説明し、Play Services Task API の概要と(隠された)使用法を紹介しました。Task は、Firebase において処理時間が予測できず、かつメインスレッド以外で実行すべき処理に対応するための手段です。Task を使用すると、リスナーによってメインスレッドに戻って処理結果を扱うようにすることもできます。ただ、この記事で紹介した Task の機能は、ほんの一部にすぎません。次の記事では、さまざまな Task リスナーを紹介しますので、ぜひ自身のユースケースに応じて最適なリスナーを選択してください。

質問がありましたら、Twitter でハッシュタグ #AskFirebase を検索するか、firebase-talk Google Group をご利用ください.専用の Firebase Slack チャネルも用意しています。Twitter で @CodingDoug のフォローもよろしくお願いします。


Posted by Khanh LeViet - Developer Relations Team
Share on Twitter Share on Facebook