この記事は Developer Advocate、Doug Stevenson による The Firebase Blog の記事 "Using Android Architecture Components with Firebase Realtime Database (Part 3)" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。



ライフサイクルに対応した Android Architecture Components と Firebase Realtime Database を組み合わせる方法を紹介した本シリーズのパート 3 にようこそ。パート 1 では、まずデータベース リスナーを使って、データベースのデータ変更に応じて UI を最新に保つ単純な Activity を作成しました。続いて Activity のライフサイクル内でリスナーを扱うボイラープレートを削除するため、LiveData と ViewModel を使うよう変更しました。パート 2 では、リファクタリングを行って Activity から Realtime Database を参照している部分をすべて削除し、パフォーマンスを向上させる仕組みも実装しました。これは、データ操作の負荷が大きすぎてメインスレッドでは行えない場合に備えて、MediatorLiveData といくつかのスレッドを用いて最適化するというものでした。

このコードには、もう 1 つ改善余地があります。データベース リスナーが受け取るデータの量によっては、パフォーマンスに大きな影響が出る可能性があります。これは、onActive() メソッドと onInactive() メソッドで、FirebaseQueryLiveData の実装がデータベース リスナーをどのように扱うかに関連しています。現在のコードをもう一度ご覧ください。
public class FirebaseQueryLiveData extends LiveData<DataSnapshot> {
    private static final String LOG_TAG = "FirebaseQueryLiveData";

    private final Query query;
    private final MyValueEventListener listener = new MyValueEventListener();

    public FirebaseQueryLiveData(Query query) {
        this.query = query;
    }

    public FirebaseQueryLiveData(DatabaseReference ref) {
        this.query = ref;
    }

    @Override
    protected void onActive() {
        query.addValueEventListener(listener);
    }

    @Override
    protected void onInactive() {
        query.removeEventListener(listener);
    }

    private class MyValueEventListener implements ValueEventListener {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            setValue(dataSnapshot);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
            Log.e(LOG_TAG, "Can't listen to query " + query, databaseError.toException());
        }
    }
}

重要な点は、onActive() でデータベース リスナーが追加され、 onInactive() で削除されていることです。FirebaseQueryLiveData を使う Activity は、このコードを onCreate() で実行しています。
HotStockViewModel viewModel = ViewModelProviders.of(this).get(HotStockViewModel.class);
LiveData<DataSnapshot> liveData = viewModel.getDataSnapshotLiveData();

liveData.observe(this, new Observer<DataSnapshot>() {
    @Override
    public void onChanged(@Nullable DataSnapshot dataSnapshot) {
        if (dataSnapshot != null) {
            // update the UI here with values in the snapshot
        }
    }
});

オブザーバは Activity のライフサイクルに従います。LiveData は、Activity の状態が STARTED または RESUMED である場合、オブザーバがアクティブであると見なします。また、Activity のライフサイクルが DESTROYED 状態である場合、オブザーバはそれを非アクティブであると見なします。LiveData オブジェクトに 1 つでもアクティブなオブザーバがあれば onActive() メソッドが呼ばれ、LiveData オブジェクトにアクティブなオブザーバがなければ onInactive() メソッドが呼ばれます。では、Activity が起動してから構成変更(端末の回転など)が起きるとどうなるでしょうか。一連のイベント(FirebaseQueryLiveData を監視している UI コントローラが 1 つだけである場合)は、次のようになります。
  1. Activity が起動する。
  2. LiveData が監視されてアクティブになり、onActive() メソッドが呼ばれる。
  3. データベース リスナーが追加される。
  4. データを受信し、UI が更新される。
  5. 端末が回転し、Activity が破棄される。
  6. LiveData の監視が解除されて非アクティブになり、onInactive() メソッドが呼ばれる。
  7. データベース リスナーが削除される。
  8. 元の Activity に代わって新しい Activity が開始される。
  9. LiveData が監視されて再びアクティブになり、onActive() メソッドが呼ばれる。
  10. データベース リスナーが追加される。
  11. データを受信し、UI が更新される。

データベース リスナーを扱う部分は、太字で示しています。ここからわかるように、Activity の構成変更が起きると、リスナーが削除され、再び追加されます。この手順によって、2 回目となる Realtime Database サーバーとの往復が発生します。そして、たとえ結果が変わっていなかったとしても、この 2 回目のクエリによる結果データがすべて取得されます。もちろん、LiveData は既に最新データのスナップショットを保持しているので、これは無駄な操作です。エンドユーザーのデータ料金という点でも、Firebase プロジェクトでの割り当ての集計や課金という点でも、この余分なクエリは不要です。

不要なクエリを避ける方法


LiveData オブジェクトがアクティブになったり非アクティブになる仕組みは、簡単に変えられるものではありません。しかし、Activity に構成変更が発生する場合、状態がどのくらい早く変わるかについて多少の予測を行うことはできます。ここでは、構成変更は 2 秒以内に完了する(通常はこれよりもかなり早く完了します)と仮定してみましょう。その場合、考えられる戦略の 1 つに、onInactive() が呼ばれた後、少し待ってから FirebaseQueryLiveData にデータベース リスナーを削除させる方法があります。この実装を次に示します。FirebaseQueryLiveData に若干の変更と追加を行ったものです。
private boolean listenerRemovePending = false;
private final Handler handler = new Handler();
private final Runnable removeListener = new Runnable() {
    @Override
    public void run() {
        query.removeEventListener(listener);
        listenerRemovePending = false;
    }
};

@Override
protected void onActive() {
    if (listenerRemovePending) {
        handler.removeCallbacks(removeListener);
    }
    else {
        query.addValueEventListener(listener);
    }
    listenerRemovePending = false;
}

@Override
protected void onInactive() {
    // Listener removal is schedule on a two second delay
    handler.postDelayed(removeListener, 2000);
    listenerRemovePending = true;
}

ここでは、LiveData が非アクティブになった後、Handler を利用してデータベース リスナーの削除を 2 秒後にスケジューリングしています(具体的には、削除を行う Runnable コールバックを送信しています)。2 秒経過する前に再度アクティブになった場合は、単に Handler からスケジューリングされた作業を削除し、リスナーがリッスンを継続できるようにします。これで、ユーザーにもお財布にも優しくなりましたね!

皆さんのアプリでは、ライフサイクル対応の Android Architecture Components と Firebase を組み合わせて使っていますか?それはうまく動作しているでしょうか?Google グループ firebase-talk で行われている Firebase の活発な議論にぜひご参加ください。


Reviewed by Khanh LeViet - Developer Relations Team