Tracelet Sync: ネットワークの物語
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'") );
シナリオ 6: テレマティクス バックエンド
検討した概念: テレマティクス同期、イベント ペイロード スキーマ、個別のエンドポイント
問題
アプリはすでに位置情報をストリーミングしています。現在、製品チームは、運転行動 (急ブレーキ、急加速、コーナリング、スピード超過) もドライバーごとにスコアリングしたいと考えています。これらのイベントはバックグラウンドで記録されます。通常は、電話に信号がなく、誰も画面を見ていないときに記録されます。これらは最も失うことが許されない行であり、「再試行」をタップするユーザーは周囲にいません。
Tracelet Sync がそれを解決する方法
-
syncTelematicsでオプトイン 運転イベントと衝撃イベントは「常に」ローカルに保持されます。これがTracelet.getTelematicsEvents()の読み取り値です。それらのアップロードはデフォルトでオプトインおよびオフになっているため、既存の統合のペイロードがその下で変更されることはありません。http: tl.HttpConfig( url: 'https://api.example.com/locations', method: tl.HttpMethod.post, autoSync: true, batchSync: true, maxBatchSize: 50, syncTelematics: true, // ← upload driving/impact events too ), -
1 つのリクエスト、1 つの無線ウェイク (デフォルト)
syncTelematics: trueを使用し、telematicsUrlを使用しない場合、イベントはlocation配列の隣にある ルートレベルのtelematics配列として位置情報リクエストに乗ります。フラッシュは 1 つの POST のままです。バックグラウンド無線は 2 回ではなく 1 回起動します。{ "location": [ /* ...your usual location records... */ ], "telematics": [ { "id": 412, "event_type": "harsh_braking", "severity": 0.82, "speed": 18.4, "value": 0.47, "latitude": 24.8607, "longitude": 67.0011, "timestamp": "2026-08-20T09:14:02.000Z", "synced": false } ] }フィールド タイプ 意味 非トランスレート 翻訳1遅れ ローカル SQLite 行の主キー。インストールごとに昇順 — 側で重複を排除するために使用します。 非トランスレート 翻訳1遅れ harsh_braking、harsh_acceleration、harsh_cornering、speeding、または影響の種類 (potential_crash、crash、potential_fall、fall)。非トランスレート 翻訳1遅れ 正規化された 0.0–1.0— イベントが検出しきい値をどれだけ超えたか。非トランスレート 翻訳1遅れ イベント時の速度 (m/s)。インパクトの場合は、入るスピードが速くなります。 非トランスレート 翻訳1遅れ severityの背後にある物理的大きさ: 過酷な出来事や衝撃の場合は g、速度超過の場合は制限を超える km/h。latitude/longitudedoubleそれが起こった場所。 非トランスレート 翻訳1遅れ ISO-8601。 非トランスレート 翻訳1遅れ 読み取られたときの行の状態 — 回線上のイベントについては常に falseです。speedとvalueは 3.8.3 で永続化行に追加されました。古いインストールによって記録されたイベントは、両方の0をレポートします。列は NULL 不可なので、アップグレードされたデータベースでは古い行と本物のゼロを区別できません。 -
バッチ処理と再試行 フラッシュごとに最大 250 の非同期イベントが、最も古いものから順に送信されます。この境界は、ロケーション バッチのみのサイズを設定する
maxBatchSizeとは独立しています。イベントは、イベントを送信したリクエストが成功した場合にのみ*、同期済みとしてマークされます。 POST が失敗した場合 (オフライン、401、503)、場所とまったく同じように、ドロップされるのではなく、次の試行のためにキューに入れられたままになります。
これらは マーク されており、削除されていないことに注意してください。アップロードされたイベントは
Tracelet.getTelematicsEvents()に表示されたままになるため、アプリは引き続きドライバーに独自の履歴を表示できます。同期された末尾は最新の 1000 行までトリミングされるため、テーブルが永久に拡大することはありません。同期されていない行は決してトリミングされません。 -
別のエンドポイント (
telematicsUrl) バックエンドが運転イベントを場所以外の場所にルーティングする場合は、telematicsUrlを設定します。その後、イベントは、位置ペイロードに乗るのではなく、独自の POST (本体{"telematics": [...]}、urlと同じヘッダー、タイムアウト、再試行、SSL 固定を使用) で送信されます。http: tl.HttpConfig( url: 'https://api.example.com/locations', syncTelematics: true, telematicsUrl: 'https://api.example.com/telematics', ),バッテリーのコストは見た目よりも小さいです。両方のリクエストは 同じ フラッシュ内で連続して実行されるため、2 番目のリクエストはすでに起動している無線を再利用します。バッテリーを消耗させるのは、独自のスケジュールで実行される 2 番目のアップローダーですが、これはそうではありません。テレマティクスのみのフラッシュ (イベントがキューに入れられ、新しいロケーションなし) は、空のロケーション POST を送信するのではなく、ロケーション POST を完全にスキップします。
後で接続されたパスに戻るには、
nullではなく 空の文字列 を渡します。構成は置換されずにマージされるため、nullは「すでに存在するものはすべて残す」ことを意味します。 -
カスタムボディビルダーも注目 本体を自分で形成する場合、非同期イベントは、上の表と同じフィールド名を使用して、場所とともにコンテキストに到着します。
Tracelet.setSyncBodyBuilder((context) async { return { 'points': context.locations, 'events': context.telematics, // driving/impact events }; });syncTelematicsが有効でない限り、context.telematicsは空です。
旅行は永続化または同期されません。 完了した旅行を Dart コールバックに送信する Tracelet.onTrip() は、旅行が SDK から出る「唯一」の方法です。旅行の状態はメモリに保持され、SQLite に書き込まれることはなく、HTTP エンドポイントに送信されることもありません。トリップが終了する前に start() を呼び出した分離がなくなった場合、トリップ イベントはまったく生成されません。トリップ イベントを失う唯一の方法はオフラインではありません。トリップ永続性がリリースされるまで、存続するために必要な場合は、onTrip() 内にトリップを自分で保存してください。 #356 を参照してください。