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

Firebase アプリケーションは、アプリのネットワーク接続が一時的に失われても機能します。さらに Firebase には、データのローカルへの保存、プレゼンスの管理、レイテンシの処理を行うためのツールが用意されています。

ディスクの永続性

Firebase アプリは、一時的なネットワークの中断を自動的に処理します。 キャッシュされたデータはオフライン中にも使用でき、ネットワーク接続が回復すると書き込み内容が再送信されます。

ディスクの永続性を有効にすると、アプリのデータは端末にローカルに書き込まれるため、オフラインになってもアプリの状態を維持できます。これはユーザーまたはオペレーティング システムがアプリを再起動した場合でも変わりません。

ディスクの永続性は、わずか 1 行のコードで有効にすることができます。

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

永続性の処理

永続性を有効にすると、オンライン中に Firebase Realtime Database クライアントによって同期されるすべてのデータがディスクに永続化され、オフラインになってもそれらのデータを使用できます。これはユーザーまたはオペレーティング システムがアプリを再起動した場合でも変わりません。つまり、アプリはキャッシュに保存されているローカルデータを使用して、あたかもオンラインであるかのように動作します。 ローカルの更新に対するリスナーのコールバックは引き続き発生します。

Firebase Realtime Database クライアントは、アプリのオフライン中に実行されたすべての書き込みオペレーションのキューを自動的に保持します。 永続性が有効になっている場合はこのキューもディスクに永続化されるため、ユーザーまたはオペレーティング システムがアプリを再起動したときにもすべての書き込みが維持されます。アプリの接続が回復すると、すべてのオペレーションが Firebase Realtime Database サーバーに送信されます。

アプリで Firebase Authentication を使用している場合、Firebase Realtime Database クライアントはアプリの再起動後もユーザーの認証トークンを保持します。 アプリのオフライン中に認証トークンが期限切れになった場合、再認証が行われるまでクライアントは書き込みオペレーションを一時停止します。一時停止しないと、セキュリティ ルールによって書き込みオペレーションが失敗する可能性があります。

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

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

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

これらの場所にあるデータは、参照にアクティブ リスナーが設定されていない場合でも自動的にダウンロードされ、データの同期が維持されます。次のコード行で同期を解除できます。

scoresRef.keepSynced(false);

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

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

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

たとえば、次のコードは、スコアが下位から 4 番目までのアイテムを Firebase Realtime Database に照会します。

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 つのアイテムを照会した場合、上記のクエリで下位 4 つのアイテムがすべて読み込まれているため、このクエリは下位 2 つのアイテムを正常に返します。

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 頭つの恐竜の 'child added' イベントが発生します。ただし、'value' イベントは発生しません。これは、オンライン中にそのクエリが実行されていないためです。

オフライン中にアプリが下位 6 つのアイテムをリクエストした場合、キャッシュに保存された 4 つのアイテムの 'child added' イベントがすぐに取得されます。端末がオンラインに戻ると、データがサーバーと同期され、残り 2 つの 'child added' イベントと 'value' イベントが取得されます。

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

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

プレゼンスの管理

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

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 サーバー上に存続します。サーバーはセキュリティをチェックして、ユーザーにリクエストされた書き込みイベントを実行する権限があることを確認し、書き込みイベントが無効な場合はアプリに通知します。サーバーはその後、接続をモニタリングします。いずれかの時点で接続がタイムアウトした場合、または 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(ブール値)は、その値がクライアントの状態に依存するため、異なる Realtime Database クライアント間で同期されません。言い換えると、あるクライアントで読み取った /.info/connected の値が false であったとしても、別のクライアントで読み取った値も false であるという保証はありません。

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

レイテンシの処理

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

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

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

クロックのずれ

firebase.database.ServerValue.TIMESTAMP は精度が高く、ほとんどの読み取り / 書き込みオペレーションに適していますが、これは Firebase Realtime Database サーバーに対するクライアントのクロックのずれを推定するためにも役立つことがあります。場所 /.info/serverTimeOffset にコールバックをアタッチしてミリ秒単位の値を取得し、この値をローカルのレポート時刻(ミリ秒単位のエポックタイム)に加算してサーバー時刻を推定できます。このオフセットの精度はネットワークのレイテンシによる影響を受ける可能性があるため、主にクロック時刻の大きな(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");
  }
});

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

接続解除のオペレーションを接続状態のモニタリングやサーバーのタイムスタンプと組み合わせることで、ユーザー プレゼンス システムを構築できます。このシステムでは、各ユーザーが、Realtime Database クライアントがオンラインであるかどうかを示すデータをデータベースの場所に保存します。クライアントは、オンラインになったときにこの場所を 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");
  }
});

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

ご不明な点がありましたら、Google のサポートページをご覧ください。