Android でのオフライン機能の有効化

Firebase アプリはオフラインでも適切に動作します。また、オフラインでの操作性をさらに高めるための機能をいくつか備えています。ディスクの永続性を有効にすると、アプリを再起動した後でも、そのアプリの状態をすべて保持できます。Firebase では、プレゼンスや接続状態をモニタリングするためのツールをいくつか用意しています。

ディスクの永続性

Firebase アプリは、ネットワークが一時的に中断しても自動的に対処します。キャッシュに保存されたデータはオフラインでも引き続き利用できます。ネットワークへの接続が回復すると、書き込み内容が再送信されます。ディスクの永続性を有効にすると、アプリを再起動した後でも、そのアプリの状態をすべて保持できます。ディスクの永続性は、わずか 1 行のコードで有効にすることができます。

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

ディスクの永続性を有効にすると、アプリが再起動されても、同期されたデータと書き込みがディスクに永続化されるので、オフラインの状況でアプリがシームレスに動作します。

永続性の処理

永続性を有効にすると、アプリを再起動しても、オンライン中に同期したあらゆるデータがディスクに永続化され、オフラインで利用できるようになります。つまり、キャッシュに保存されたローカルデータを使用して、アプリがオンラインの場合と同様に動作するということです。ローカルの更新に対してリスナーのコールバックは引き続き発生します。

Firebase Realtime データbase クライアントは、アプリケーションのオフライン中に実行された、すべての書き込み操作のキューを自動的に保持します。永続性が有効になると、このキューもディスクに永続化されるので、アプリを再起動しても、すべての書き込み内容が利用できます。アプリの接続が回復すると、すべての操作がサーバーに送信されます。

アプリで Firebase Authentication を使用している場合、クライアントは再起動されてもユーザーの認証トークンを保持します。アプリのオフライン中に認証トークンが期限切れになった場合、再認証が行われるまで、クライアントは書き込み操作を一時停止します。一時停止しなかった場合、セキュリティ ルールが理由で書き込み操作に失敗することがあります。

データを最新の状態に保つ

Firebase Realtime Database では、アクティブ リスナー用にデータのローカルコピーが同期され、保存されます。さらに、特定の場所を同期した状態に保つことができます。

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

クライアントでは、参照にアクティブ リスナーが存在しない場合でも自動的に、その場所にあるデータがダウンロードされ、同期が保たれます。次のコード行で同期をオフに戻すことができます。

scoresRef.keepSynced(false);

デフォルトでは、以前に同期されたデータのうち 10 MB がキャッシュに保存されます。ほとんどのアプリケーションでは、この量で十分です。キャッシュが設定済みのサイズよりも大きくなった場合、Firebase Realtime Database は最も長い間使われていないデータを消去します。同期が保たれているデータはキャッシュから消去されません。

オフラインでのデータのクエリ

Firebase Realtime Database は、オフラインのときでも使用できるように、クエリから返されたデータを保存します。オフライン中に作成されたクエリに対しては、Firebase Realtime Database は以前に読み込まれたデータでそのまま動作します。要求されたデータが読み込まれていない場合、Firebase Realtime Database がローカル キャッシュからデータを読み込みます。オンラインに戻ったときに、データが読み込まれて、クエリが反映されます。

たとえば、次に示すアプリのコード部分では、Firebase Realtime Database 内の、スコアで下位 4 つのアイテムに対するクエリを実行しています。

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChild) {
      System.out.println("The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }
});

今度は、ユーザーが接続を失ってオフラインになり、アプリを再起動した場合について考えてみましょう。オフラインのまま、同じ場所から下位 2 つのアイテムに対するクエリを実行します。このクエリでは、下位 2 つのアイテムが正常に返されます。これは、上記のクエリで 4 つのアイテムをすべて読み込んでいたからです。

scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChild) {
        System.out.println("The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
      }
});

上記の例では、永続化されたキャッシュから上位 2 つのスコアの恐竜に対して、Firebase Realtime Database クライアントが 'child added' イベントを発生させます。ただし、'value' イベントが発生することはありません。これは、そのクエリをオンライン中に実行したことがないからです。

オフライン中に下位 6 つのアイテムをリクエストした場合、まずは、キャッシュに保存された 4 つのアイテムについて 'child added' イベントを取得することになります。オンラインに戻ったとき、Firebase Realtime Database クライアントはサーバーと同期して、最後の 2 つの 'child added' イベントと 'value' イベントを取得することになります。

オフラインでのトランザクションの処理

アプリのオフライン中に実行されたトランザクションはすべてキューに入れられます。アプリとネットワークの接続が回復すると、トランザクションがサーバーに送信されます。

プレゼンスの管理

リアルタイム アプリケーションでは、クライアントが接続したときや接続を解除したときを検出できると役に立つことがよくあります。たとえば、クライアントが接続を解除したら、そのユーザーを「offline」としてマークするなどです。

Firebase Database クライアントでは、クライアントが Firebase Database サーバーから接続を解除されたときにデータベースにデータを書き込むことができるようにする、シンプルなプリミティブを提供しています。このような更新はクライアントがクリーンに接続を解除したかどうかに関係なく発生するので、接続が削除された場合やクライアントがクラッシュした場合でも、更新でデータを確実にクリーンアップすることができます。接続が解除された時点で、設定、更新、削除など、あらゆる書き込み操作を実行できます。

onDisconnect プリミティブを使用して、接続の解除と同時にデータを書き込むシンプルな例を次に示します。

DatabaseRef presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!");

onDisconnect の仕組み

onDisconnect() 操作が設定されると、この操作は Firebase Realtime Database サーバー上で有効になります。サーバーはセキュリティをチェックして、ユーザーがリクエストされた書き込みイベントを実行できることを確認します。書き込みイベントが無効な場合は、ユーザーに通知します。サーバーはその後、接続をモニタリングします。なんらかの時点で接続のタイムアウトが発生した場合や、クライアントによって接続が能動的に閉じられた場合は、サーバーはもう一度セキュリティをチェックして(操作がまだ有効であることを確認して)から、イベントを呼び出します。

クライアントは書き込み操作に対するコールバックを使用して、onDisconnect が正しくアタッチされたことを確認できます。

presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() {
    @Override
    public void onComplete(DatabaseError error, DatabaseReference firebase) {
        if (error != null) {
            System.out.println("could not establish onDisconnect event:" + error.getMessage());
        }
    }
});

.cancel() を呼び出して onDisconnect イベントをキャンセルすることも可能です。

OnDisconnect onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.setValue("I disconnected");
// some time later when we change our minds
onDisconnectRef.cancel();

接続状態の検出

数あるプレゼンス関連の機能の中でも、オンラインまたはオフラインになったときをクライアント側で認識できる機能は有用です。Firebase Realtime Database クライアントには、クライアントの接続状態が変化すると、そのたびに更新される /.info/connected という特別な場所が用意されています。次に例を示します。

DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    boolean connected = snapshot.getValue(Boolean.class);
    if (connected) {
      System.out.println("connected");
    } else {
      System.out.println("not connected");
    }
  }

  @Override
  public void onCancelled(DatabaseError error) {
    System.err.println("Listener was cancelled");
  }
});

/.info/connected は、値がクライアントの状態に依存するため、クライアント間で同期されないブール値です。言い換えると、あるクライアントが読み取った /.info/connected の値が false である場合に、別のクライアントが読み取った値も false であるという保証はありません。

Android では、Firebase は自動的に帯域幅と電池の使用量を減らすために、接続状態を管理します。クライアントがアクティブなリスナーまたは保留中の書き込み操作がなく、goOffline メソッドによって明示的に接続を解除されていない場合、Firebase は非アクティブになってから 60 秒後に接続を閉じます。

レイテンシの処理

サーバーのタイムスタンプ

Firebase Realtime Database サーバーには、サーバー上で生成されたタイムスタンプをデータとして挿入するメカニズムが用意されています。この機能を onDisconnect と組み合わせることで、クライアントが接続を解除した時刻を確実かつ簡単に記録できるようになります。

DatabaseReference userLastOnlineRef = FirebaseDatabse.getInstance().getReference("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

クロックのずれ

firebase.database.ServerValue.TIMESTAMP は大変正確で、ほとんどの読み取り / 書き込み操作に適している上、Firebase Realtime Database のサーバーに対するクライアントのクロックのずれを推定するのに役に立つこともあります。コールバックを場所 /.info/serverTimeOffset に接続して、ミリ秒単位の値を取得できます。この値は、Firebase Realtime Database クライアントにより、サーバー時刻を推定するためにローカルのレポート時刻(ミリ秒単位のエポックタイム)に加算されます。このオフセットの精度はネットワーク処理のレイテンシによる影響を受けることがあるので、主にクロック時刻の大きな(1 秒を超える)不一致を検出するのに役立つことに注意してください。

DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset")
offsetRef.addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    double offset = snapshot.getValue(Double.class);
    double estimatedServerTimeMs = System.currentTimeMillis() + offset;
  }

  @Override
  public void onCancelled(DatabaseError error) {
    System.err.println("Listener was cancelled");
  }
});

プレゼンスのサンプルアプリ

接続解除の操作を接続状態のモニタリングやサーバーのタイムスタンプと組み合わせることで、ユーザー プレゼンス システムを構築できます。このシステムでは、各ユーザーが、クライアントがオンラインであるかどうかを示すデータをデータベースの場所に保存します。クライアントは、オンラインになったときにこの場所を true に設定し、接続を解除したときにタイムスタンプを設定します。このタイムスタンプは、指定されたユーザーがオンラインであった最後の時刻を示します。

両方のコマンドをサーバーに送信できるようになる前にクライアントのネットワーク接続が切断された場合に、競合状態が発生するのを回避するため、接続解除の操作をキューに入れてから、ユーザーをオンラインとしてマークする必要があるので注意してください。

次に、シンプルなユーザー プレゼンス システムを示します。

// since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = FirebaseDatabase.getInstance();
final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections");

// stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/users/joe/lastOnline");

final DatabaseReference connectedRef = database.getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    boolean connected = snapshot.getValue(Boolean.class);
    if (connected) {
      // add this device to my connections list
      // this value could contain info about the device or a timestamp too
      DatabaseReference con = myConnectionsRef.push();
      con.setValue(Boolean.TRUE);

      // when this device disconnects, remove it
      con.onDisconnect().removeValue();

      // when I disconnect, update the last time I was seen online
      lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
    }
  }

  @Override
  public void onCancelled(DatabaseError error) {
    System.err.println("Listener was cancelled at .info/connected");
  }
});

フィードバックを送信...