Tracelet Sync: ネットワークの物語
データがサーバーに到達しなければ、位置追跡は意味がありません。エレベーターに入るとき、谷間を走行するとき、Wi-Fi からセルラーに切り替えるときなど、モバイル デバイスは常に接続を切断します。 Tracelet Sync は、UI を起動せずにデータ配信を確実にするように設計された、オフラインファーストのバッテリー対応ネットワーキング エンジンです。
Tracelet 3.2.0 更新: HTTP 同期ロジックが tracelet_sync モジュールに移動されました。ネットワーク同期が必要な場合は、このモジュールを含める必要があります。
ネットワークに同期する方法
Tracelet Core が位置をキャプチャしている間、tracelet_sync はバックグラウンド HTTP エンジンとして位置をサーバーに配信します。これを有効にするには、追跡を開始する前に TraceletSync.ready() を初期化します。
import 'package:tracelet_sync/tracelet_sync.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Configure the Sync Engine
await TraceletSync.ready(SyncConfig(
url: 'https://your-api.com/locations',
method: 'POST',
autoSyncThreshold: 10, // Sync every 10 locations
autoSyncDelay: 10000, // Wait 10s before pushing
syncInterval: 0, // (Optional) Seconds between repeating queue flushes; 0 = off
batchSync: true, // Send as a JSON array
maxBatchSize: 250, // Up to 250 locations per request
headers: {
'Authorization': 'Bearer YOUR_TOKEN'
},
));
// 2. Configure and start Tracelet Core as usual
await Tracelet.ready(Config.balanced());
await Tracelet.start();
}構成が完了すると、エンジンは以下のすべてのシナリオを自動的に処理します。
シナリオ 1: 山ハイキング
検討した概念: オフライン キューイング、バッチ同期、デバウンス
問題
マークはフィットネス アプリを使用して、山を通る 3 時間のハイキングを記録しています。彼は携帯電話サービスをまったく利用していません。アプリが一歩を踏み出すたびに HTTP POST を試行すると失敗し、信号の検索でバッテリーが無駄になり、位置ポイントが永久に失われます。最終的に車に戻ると、彼のルートは白地図のようになります。
Tracelet Sync がそれを解決する方法
-
オフライン SQLite 永続性 (
autoSyncThreshold) Mark には信号がないため、Tracelet は HTTP リクエストの送信を直ちに停止します。代わりに、すべての場所がローカルの SQLite データベースに安全に保存されます。autoSyncThreshold: 100を設定します。これは、少なくとも 100 ポイントがデータベースにキューに入れられるまで、Tracelet はネットワーク無線をスリープ解除しようとさえしないことを意味します。 -
デバウンス同期 (
autoSyncDelay) マークが車で山を下り、4G を取り戻したとき、突然 500 か所のキューができました。autoSyncDelay: 10000は、500 の即時 HTTP リクエストを発行する (これにより携帯電話がフリーズする) 代わりに、Tracelet に 10 秒待つように指示します。データの急速な流入をデバウンスし、接続を安定させます。 -
バッチ同期 (
batchSyncおよびmaxBatchSize) 500 個の個別のPOSTリクエストの代わりに、batchSync: trueとmaxBatchSize: 250は場所を 2 つの大規模な JSON 配列にバンドルします。最初の 250 ポイントを送信し、サーバーがHTTP 200 OKを返すのを待ち、それらのポイントを SQLite から削除してから、次のバッチを送信します。 -
インターバルベースの同期 (
syncInterval)autoSyncDelayは 新しい 場所に反応します。時間駆動のフラッシュも必要な場合は、累積ポイント数に関係なく、固定のリズムでキューにあるものをアップロードします。syncIntervalをフラッシュ間の秒数に設定します (たとえば、syncInterval: 60はオフライン キューを 1 分に 1 回フラッシュします)。これはデバウンスと同時に実行され、デフォルトでは無効になっています (0)。
シナリオ 2: カフェ Wi-Fi に接続する
検討された概念: セルラー制限、デルタ圧縮
問題
マークはハイキングを終えてカフェに行きます。彼は海外旅行中なので、携帯電話のデータプランが非常に高価です。あなたのアプリはメガバイトの位置情報 JSON データをキューに入れており、それをローミング 4G 接続経由で送信すると費用がかかります。
Tracelet Sync がそれを解決する方法
-
セルラー制限 (
disableAutoSyncOnCellular)disableAutoSyncOnCellular: trueを設定すると、Mark が 4G に接続している間、Tracelet は同期エンジンを完全にブロックします。 SQLite では場所は安全に保たれます。彼がカフェの Wi-Fi に接続した瞬間、OS が Tracelet を起動し、同期エンジンが自動的にキューをフラッシュします。 -
デルタ エンコーディング圧縮 (
enableDeltaCompression) Wi-Fi 上でも、大規模な JSON 配列の送信には時間がかかります。 Tracelet は、バッチを送信する前に デルタ圧縮 を適用します。マークが直線で歩いた場合、緯度はあまり変化しません。すべてのポイントの完全な座標を送信する代わりに、Tracelet は最初のポイントを完全に送信し、その後、後続のポイントの 差 (デルタ) のみを送信します。deltaCoordinatePrecision: 5(精度は約 1.1 メートル) を使用すると、HTTP ペイロード サイズが 60% から 80% 削減されます。
標準ペイロード (圧縮なし):
[
{"lat": 37.774900, "lng": -122.419400},
{"lat": 37.774910, "lng": -122.419410},
{"lat": 37.774920, "lng": -122.419420}
]デルタエンコードされたペイロード (サーバーが受信するもの):
[
{"lat": 37.774900, "lng": -122.419400},
{"dLat": 10, "dLng": 10},
{"dLat": 10, "dLng": 10}
]シナリオ 3: 期限切れのセッション
検討された概念: 401 再試行、ヘッドレス コールバック、指数関数的バックオフ
問題
マークの認証トークン (JWT) はハイキング中に期限切れになりました。 Tracelet が最終的にバッチをサーバーに同期しようとすると、API は HTTP 401 Unauthorized を返します。単純な同期エンジンは、失敗したと考えてデータを削除するか、401 秒の無限ループに陥ってバッテリーを消耗してしまいます。マークの携帯電話はポケットの中にあり、画面はオフになっています。現在はログインできません。
Tracelet Sync がそれを解決する方法
- 動的ヘッダー コールバック Tracelet が 401 を受信すると、再試行する前に新しいトークンをフェッチする必要があります。フォアグラウンドとバックグラウンド (ヘッドレス) 状態の両方でこれを処理するには、コールバックを登録します。
フォアグラウンド コールバック:
tl.Tracelet.setHeadersCallback(() async {
final newJwt = await AuthAPI.refreshToken();
return {'Authorization': 'Bearer $newJwt'};
});バックグラウンド (ヘッドレス) コールバック: これは、UI を起動せずに分離された Dart エンジンで実行され、ユーザーがアプリを強制終了した場合でも同期が確実に機能します。
@pragma('vm:entry-point')
void headlessHeadersCallback(tl.HeadlessEvent event) async {
final newJwt = await AuthAPI.refreshToken();
tl.Tracelet.setDynamicHeaders({'Authorization': 'Bearer $newJwt'});
}
// Register it before runApp()
tl.Tracelet.registerHeadlessHeadersCallback(headlessHeadersCallback);- 指数バックオフ (
maxRetriesおよびretryBackoffCap) 認証サーバーがダウンして 503 を返した場合はどうなりますか? Tracelet はこれを適切に処理します。再試行を試みます。それは失敗します。 1 秒待機し (retryBackoffBase)、次に 2 秒待機し、次に 4 秒待機します。指数バックオフの上限は 60 秒 (retryBackoffCap) です。 3 回の試行 (maxRetries) の後、完全に諦め、データは SQLite に安全に残され、明日再試行されます。
シナリオ 4: カスタム サーバー スキーマ
検討した概念: カスタム同期ボディ ビルダー、スキーマ マッピング
問題
Mark の会社には、非常に特殊な非標準形式の位置データを期待するレガシー バックエンドがあります。 Tracelet のデフォルトの JSON ペイロードはサーバーに必要なスキーマと一致しないため、このアプリのためだけにバックエンド API を変更することはできません。
Tracelet Sync がそれを解決する方法
- カスタム同期ボディビルダー (
setSyncBodyBuilder) デフォルトの JSON ラッパーを使用する代わりに、Tracelet を使用すると、ネットワーク上に送信される直前に位置のバッチをインターセプトできるため、サーバーが望む任意の形状に位置をマッピングできます。
Tracelet 3.2.8 以降、このビルダーに渡される位置は、堅牢な ネストされたスキーマ (座標は coords に安全にグループ化され、アクティビティ データは activity にグループ化されます) を使用します。
Tracelet.setSyncBodyBuilder((context) async {
// Map Tracelet's nested schema to your legacy server's flat schema
final mappedPoints = context.locations.map((loc) {
final coords = loc['coords'] as Map;
final activity = loc['activity'] as Map;
return {
'lat': coords['latitude'],
'lng': coords['longitude'],
'time': loc['timestamp'],
'moving': loc['is_moving'],
'action': activity['type'],
};
}).toList();
// Return the exact JSON structure your server expects
return {
'device_id': myDeviceId,
'payload': mappedPoints,
};
});- ヘッドレス実行
トークンの更新と同様に、このカスタム ボディの構築も
registerHeadlessSyncBodyBuilder()を介してバックグラウンドで完全にヘッドレスで実行できるため、アプリが完全に終了した場合でもカスタム スキーマが構築されて送信されることが保証されます。
シナリオ 5: 配達ドライバー
検討した概念: ルート コンテキストとビジネス ロジック インジェクション
問題
バックエンドは何千もの生の座標を受け取ります。しかし、座標だけでは、ユーザーが「なぜ」そこにいたのかを知ることはできません。ドライバーは配送業務中でしたか?彼らはどの注文を配達していましたか?バックグラウンドの位置ペイロードにビジネス ロジックを直接接続して、データベース内で簡単にクエリできるようにする方法が必要です。
Tracelet Sync がそれを解決する方法
-
ルートコンテキストの設定 カスタム メタデータを Tracelet に挿入できます。
setRouteContext()を呼び出した後に記録されたすべての場所は、内部 SQLite データベース内でこのデータで自動的に永続的にタグ付けされます。await tl.Tracelet.setRouteContext( const tl.RouteContext( taskId: 'delivery-1234', driverId: 'john_doe', custom: {'shift_id': 'morning-shift-001'}, ), ); -
結果の JSON ペイロード Tracelet がバックエンドと同期すると、配列内のすべての場所に
contextオブジェクトが含まれるため、バックエンドはこの場所がどのタスクに属しているかを正確に認識できるようになります。{ "locations": [ { "coords": { "latitude": 37.7749, "longitude": -122.4194 }, "battery": { "level": 0.85, "isCharging": true }, "extras": { "your_custom_key": "your_value" }, "context": { "taskId": "delivery-1234", "driverId": "john_doe", "custom": { "shift_id": "morning-shift-001" } } } ] }
バッテリー状態の追跡
Tracelet は、位置が記録されてから数時間後にデバイスが同期する可能性がある過酷なオフライン環境向けに設計されています。現場でのデバイスの状態を把握できるように、Tracelet はすべての場所が記録された時点での正確なバッテリー状態を自動的にキャプチャします。
これには ゼロ構成 が必要です。
- エンジンは
level(たとえば、85% の場合は 0.85) およびisCharging状態をキャプチャします。 - この状態は、GPS 座標とともにオフライン SQLite データベースに安全に保存されます。
- デバイスがオンラインに戻ると、同期エンジンは現在のバッテリー状態ではなく、正確な過去のバッテリー状態を送信します。
これにより、バックエンドはルート全体にわたるバッテリーの消耗を正確に視覚化したり、ドライバーがシフト中に常にデバイスのプラグを抜いているかどうかを特定したりできます。
-
コンテキストのクリア ドライバーが配達を終了したら、コンテキストをクリアします。それ以降の場所にはタグが付けられなくなります。
await tl.Tracelet.clearRouteContext();
同期する前に、このコンテキストに基づいて内部 SQLite データベースにクエリを実行することもできます。
// Get all locations belonging to a specific delivery task
final locations = await tl.Tracelet.getLocations(
tl.SQLQuery(where: "context_task_id = 'delivery-1234'")
);