Firebaseとデバイスの時刻

Firebaseとデバイスの時刻に関するカバー画像

開発において好きなことの一つは問題解決で、解決策がないと諦めるのは好きではありませんが、この問題の解決には少し創造性が必要だったのです。

背景


多様な地域で使用されるアプリケーションにおいて、私たちはアプリのオフライン機能を高次元で維持したいと考えています。これにより、モバイルの受信状態が悪い、または全くない状態でも、スムーズに事が運ぶことができます。

他の多くの場合と同様に、Firebaseも活用しています。欠点はありますが、ネットワーク環境が悪くてもデータの転送をうまく管理し、結局はデータが必ず届きます。

最近、Firebaseに直接プッシュされたデータのタイムスタンプに誤りがあるという端的なケースが私たちの注意を引きました。多くの場合、1〜2時間の誤差があり、最初に仮定したのはタイムゾーンの問題で、簡単に扱えるはずです...

残念ながらそれほど簡単ではありませんでした。Firebaseはタイムゾーンを問題なく扱いますが、実際の問題はこれら特定のユーザーが時計を手動で、しかも間違って設定していたことでした。なぜFirebaseが真実の源を間違っていると仮定しなければならないのでしょうか。

あまり良くないアイデア


これは単純に機能すると思っていたのですが、時差でないことはまだ理解していなかったので、DateTimeの拡張を使うとこのシナリオに便利で、元々はユニットテスト内のDateTimeのモックを可能にするために導入されました。

extension ExtendedDateTime on DateTime {
  static DateTime? _customTime;

  static DateTime get current => _customTime ?? DateTime.now();

  static set customTime(DateTime customTime) {
    _customTime = customTime;
  }
}

そのため、拡張ファイルを開いて.toUTC()を追加したのですが、これが解決策ではありませんでした...

これにより実際に問題に真剣に取り組み、テストデバイスの時間をいじり始めたのですが、タイムゾーンは関係なく、時刻が正しければタイムゾーンは影響しません。

運が良ければ、FirebaseのユーティリティFieldValue.serverTimestamp()で簡単に解決できたかもしれませんが、問題のデータは配列の一部で、最も簡単な更新方法はFieldValue.arrayUnion([])を使用することです。しかし、これはserverTimeStampをサポートしておらず、setupdateメソッドのトップレベルでのみ機能します。

次のアイデアは、timezoneパッケージを試すことでしたが、これも同じロジックのルールに従い、時計が間違っていれば間違っています。

ちょうど十分にクレイジーなアイデア?


これは、私が生きている、呼吸する「ラバーダック」、つまり私の犬に相談した場合でした。私は決して彼女にコードの説明を試みることはありませんが、散歩をしているときに解決策が私のところにしばしば浮かんでくることがあります。これもその一例でした。

Flutter用の別のパッケージはntpパッケージで、こちらはユーザーのデバイスとは独立した真実の源泉であるNetwork Time Protocolサーバーから「現実の」時刻を取得することができます。

もちろんこれは自然にオンラインの呼び出しですが、私たちは時刻が必要なたびにこれを行いません。効率が悪くデータの無駄遣いであるだけでなく、最悪の状態では失敗します。

いいえ、この呼び出しは起動時とアプリ再開時にかなり頻繁に行われ、悪いネットワーク状態にも耐えうるほど小さなものです。

グローバル変数を使用して、NTP時刻とデバイス時刻の差を保存します。時計が合っているとき、その差はわずか数ミリ秒です。

  static Future<void> setTimeGap() async {
    final startDate = await NTP.now();
    final phoneNow = DateTime.now();
    final diff = startDate.difference(phoneNow);

    ClockDifference = diff;
  }

上記はこのドキュメントを取得するために書かれたシンプルな機能で、ネットワーク時刻を取得し、その後デバイス時刻を取得し、最終的にグローバルClockDifference変数に保存します。

これにより、以前に示した拡張に小さなアップデートが必要になりました。

  static DateTime get current {
    final diff = ClockDifference;
    final now = DateTime.now();

    if (diff.isNegative) {
      return now.subtract(diff.abs());
    } else if (!diff.isNegative && diff != Duration.zero) {
      return now.add(diff);
    }

    return _customTime ?? now;
  }

これを使って、時刻差がマイナスかどうかをチェックし、時刻をどちらに調整するかを知ります。そして、テストのため、または呼び出しが失敗したかもしれないので、差がDuration.zeroでないことを確認します。これはグローバルのデフォルト値でもあります。

いくつかの注意点があるとはいえ、これは上述のようにエッジケースであり、私たちのユーザーの圧倒的多数にとっては本当に必要とされないものです。しかし、これらのタイムスタンプの一部がレポートに入り、それらの中には財務計算の一部となるものもあるため、データが数時間ものずれよりも丸め誤差によって誤りがある方が安全です。

時間とは結局のところお金なのです...


この記事が面白いと感じていただけたら幸いです。何か質問、コメント、改善点があれば、コメントをお寄せください。開発の旅を楽しんでください :D

読んでいただきありがとうございました。