Skip to Content
核心概念跟踪同步

Tracelet 同步:网络故事

如果数据从未到达您的服务器,位置跟踪就毫无意义。移动设备在进入电梯、开车穿过山谷或从 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(); }

配置完成后,引擎将自动处理以下所有场景!


场景一:登山

探索的概念: 离线排队、批量同步、去抖

问题

马克正在使用您的健身应用程序记录 3 小时的山间徒步旅行。他的手机服务为零。如果您的应用程序每次执行一步时都尝试 HTTP POST,它将失败,浪费电池搜索信号,并永远丢失位置点。当他最终回到车上时,他的路线看起来就像一张空白地图。

Tracelet Sync 如何解决这个问题

  1. 离线SQLite持久化(autoSyncThreshold) 由于 Mark 没有信号,Tracelet 立即停止尝试发送 HTTP 请求。相反,每个位置都安全地存储在本地 SQLite 数据库中。我们设置 autoSyncThreshold: 100,这意味着 Tracelet 甚至不会“尝试”唤醒网络无线电,直到数据库中至少有 100 个点排队。

  2. 去抖同步 (autoSyncDelay) 当马克最终开车下山并恢复 4G 时,他突然有 500 个排队位置。 autoSyncDelay: 10000 没有立即发出 500 个 HTTP 请求(这会冻结他的手机),而是告诉 Tracelet 等待 10 秒。它可以阻止数据的快速涌入,从而使连接稳定。

  3. 批量同步(batchSync & NOTTRANS1LATE) batchSync: truemaxBatchSize: 250 将位置捆绑到两个大型 JSON 数组中,而不是 500 个单独的 POST 请求。它发送前 250 个点,等待服务器返回 HTTP 200 OK,从 SQLite 中删除这些点,然后发送下一批。

  4. 基于间隔的同步 (syncInterval) autoSyncDelay位置做出反应。如果您还想要时间驱动的刷新 - 以固定节奏上传排队的任何内容,无论累积了多少点 - 将 syncInterval 设置为刷新之间的秒数(例如 syncInterval: 60 每分钟刷新一次离线队列)。它与去抖动一起运行,并且默认情况下处于禁用状态 (0)。


场景2:连接咖啡厅Wi-Fi

探索的概念: 蜂窝限制、Delta 压缩

问题

马克结束了徒步旅行,去了一家咖啡馆。他要出国旅行,因此他的蜂窝数据套餐非常昂贵。您的应用程序已排队兆字节的位置 JSON 数据,通过他的漫游 4G 连接发送该数据将花费他金钱。

Tracelet Sync 如何解决这个问题

  1. 细胞限制 (disableAutoSyncOnCellular) 通过设置 disableAutoSyncOnCellular: true,当 Mark 使用 4G 时,Tracelet 会完全阻止同步引擎。这些位置在 SQLite 中保持安全。当他连接到咖啡馆的 Wi-Fi 时,操作系统会唤醒 Tracelet,同步引擎会自动刷新队列。

  2. Delta编码压缩(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} ]

Delta编码有效负载(您的服务器接收的内容):

[ {"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 如何解决这个问题

  1. 动态标头回调 当 Tracelet 收到 401 时,它必须在重试之前获取新令牌。您可以注册回调来在前台和后台(无头)状态下处理此问题。

前台回调:

tl.Tracelet.setHeadersCallback(() async { final newJwt = await AuthAPI.refreshToken(); return {'Authorization': 'Bearer $newJwt'}; });

后台(无头)回调: 它在独立的 Dart 引擎中运行,无需唤醒您的 UI,即使用户强制退出应用程序,也能确保同步正常进行。

@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);
  1. 指数退避(maxRetriesretryBackoffCap 如果您的身份验证服务器关闭并返回 503 该怎么办? Tracelet 优雅地处理了这个问题。它尝试重试。它失败了。它等待 1 秒 (retryBackoffBase),然后等待 2 秒,然后等待 4 秒。指数退避的上限为 60 秒 (retryBackoffCap)。 3 次尝试后 (maxRetries),它完全放弃,将数据安全地留在 SQLite 中以便明天再试。

场景 4:自定义服务器架构

探索的概念: 自定义同步车身构建器、架构映射

问题

Mark 的公司有一个传统后端,需要非常具体的非标准格式的位置数据。 Tracelet 的默认 JSON 负载与其服务器所需的架构不匹配,并且他们无法仅为此应用程序更改后端 API。

Tracelet Sync 如何解决这个问题

  1. 自定义同步车身生成器 (setSyncBodyBuilder) Tracelet 不使用默认的 JSON 包装器,而是允许您在通过网络发送一批位置之前拦截它们,从而允许您将它们映射到服务器所需的任何形状。

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, }; });
  1. 无头执行 就像令牌刷新一样,此自定义主体构建也可以通过 registerHeadlessSyncBodyBuilder() 在后台完全无头执行,确保即使应用程序完全终止也可以构建和发送自定义架构。

场景5:送货司机

探索的概念: 路由上下文和业务逻辑注入

问题

您的后端收到数千个原始坐标。但仅凭坐标并不能告诉您用户“为什么”在那里。司机是在执行送货任务吗?他们正在交付哪份订单?您需要一种将业务逻辑直接附加到后台位置负载的方法,以便您可以轻松地在数据库中查询它。

Tracelet Sync 如何解决这个问题

  1. 设置路由上下文 您可以将自定义元数据注入 Tracelet。调用 setRouteContext() 后记录的每个位置将自动在内部 SQLite 数据库中使用此数据永久标记。

    await tl.Tracelet.setRouteContext( const tl.RouteContext( taskId: 'delivery-1234', driverId: 'john_doe', custom: {'shift_id': 'morning-shift-001'}, ), );
  2. 生成的 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 数据库中。
  • 当设备重新上线时,同步引擎会传输准确的历史电池状态,而不是当前电池状态。

这使您的后端能够准确地可视化路线上的电池消耗情况,或者确定司机在轮班期间是否始终拔掉设备插头。

  1. 清理上下文 当司机完成送货后,清除上下文。后续位置将不再被标记。

    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'") );