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 如何解决这个问题
-
离线SQLite持久化(
autoSyncThreshold) 由于 Mark 没有信号,Tracelet 立即停止尝试发送 HTTP 请求。相反,每个位置都安全地存储在本地 SQLite 数据库中。我们设置autoSyncThreshold: 100,这意味着 Tracelet 甚至不会“尝试”唤醒网络无线电,直到数据库中至少有 100 个点排队。 -
去抖同步 (
autoSyncDelay) 当马克最终开车下山并恢复 4G 时,他突然有 500 个排队位置。autoSyncDelay: 10000没有立即发出 500 个 HTTP 请求(这会冻结他的手机),而是告诉 Tracelet 等待 10 秒。它可以阻止数据的快速涌入,从而使连接稳定。 -
批量同步(
batchSync& NOTTRANS1LATE)batchSync: true和maxBatchSize: 250将位置捆绑到两个大型 JSON 数组中,而不是 500 个单独的POST请求。它发送前 250 个点,等待服务器返回HTTP 200 OK,从 SQLite 中删除这些点,然后发送下一批。 -
基于间隔的同步 (
syncInterval)autoSyncDelay对新位置做出反应。如果您还想要时间驱动的刷新 - 以固定节奏上传排队的任何内容,无论累积了多少点 - 将syncInterval设置为刷新之间的秒数(例如syncInterval: 60每分钟刷新一次离线队列)。它与去抖动一起运行,并且默认情况下处于禁用状态 (0)。
场景2:连接咖啡厅Wi-Fi
探索的概念: 蜂窝限制、Delta 压缩
问题
马克结束了徒步旅行,去了一家咖啡馆。他要出国旅行,因此他的蜂窝数据套餐非常昂贵。您的应用程序已排队兆字节的位置 JSON 数据,通过他的漫游 4G 连接发送该数据将花费他金钱。
Tracelet Sync 如何解决这个问题
-
细胞限制 (
disableAutoSyncOnCellular) 通过设置disableAutoSyncOnCellular: true,当 Mark 使用 4G 时,Tracelet 会完全阻止同步引擎。这些位置在 SQLite 中保持安全。当他连接到咖啡馆的 Wi-Fi 时,操作系统会唤醒 Tracelet,同步引擎会自动刷新队列。 -
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 如何解决这个问题
-
动态标头回调 当 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); -
指数退避(
maxRetries和retryBackoffCap) 如果您的身份验证服务器关闭并返回 503 该怎么办? Tracelet 优雅地处理了这个问题。它尝试重试。它失败了。它等待 1 秒 (retryBackoffBase),然后等待 2 秒,然后等待 4 秒。指数退避的上限为 60 秒 (retryBackoffCap)。 3 次尝试后 (maxRetries),它完全放弃,将数据安全地留在 SQLite 中以便明天再试。
场景 4:自定义服务器架构
探索的概念: 自定义同步车身构建器、架构映射
问题
Mark 的公司有一个传统后端,需要非常具体的非标准格式的位置数据。 Tracelet 的默认 JSON 负载与其服务器所需的架构不匹配,并且他们无法仅为此应用程序更改后端 API。
Tracelet Sync 如何解决这个问题
-
自定义同步车身生成器 (
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, }; }); -
无头执行 就像令牌刷新一样,此自定义主体构建也可以通过
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 ), -
一次请求,一次无线电唤醒(默认) 使用
syncTelematics: true且不使用telematicsUrl时,事件将位置请求作为 根级telematics数组 处理,位于location数组旁边。刷新保持单个 POST — 后台无线电唤醒一次,而不是两次。{ "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 } ] }领域 类型 意义 不翻译迟到 不翻译晚点 本地 SQLite 行的主键。每次安装升序——用它来删除重复数据。 不翻译迟到 不翻译晚点 harsh_braking、harsh_acceleration、harsh_cornering、speeding或影响类型(potential_crash、crash、potential_fall、fall)。不翻译迟到 不翻译晚点 标准化 0.0–1.0— 事件超出检测阈值的距离。不翻译迟到 不翻译晚点 事件的速度以米/秒为单位。对于冲击,速度。 不翻译迟到 不翻译晚点 severity背后的物理强度:g 表示恶劣事件和影响,km/h 超过限制 表示超速。不传输迟到 / 不传输迟到 不翻译2晚 事情发生的地方。 不翻译迟到 不翻译晚点 ISO-8601。 不翻译迟到 不翻译晚点 读取行时的状态 — 对于线路上的事件,始终为 false。speed和value在 3.8.3 中加入了持久化行。较旧的安装记录的事件对两者都报告0:这些列不可为空,因此升级后的数据库无法区分旧行和真正的零。 -
批处理和重试 每次刷新最多 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', ),电池成本比看起来要小:两个请求在“相同”刷新中连续进行,因此第二个请求重复使用已经唤醒的无线电。消耗电池的是第二个上传程序按自己的时间表运行,而这不是这样的。仅远程信息处理刷新(事件排队,无新位置)完全跳过位置 POST,而不是发送空位置。
要稍后返回附加路径,请传递空字符串而不是
null- 配置被合并,而不是替换,因此null意味着“保留已经存在的内容”。 -
定制健身者也看到它们 如果您自己塑造主体,则未同步的事件将到达位置旁边的上下文,并使用与上表相同的字段名称:
Tracelet.setSyncBodyBuilder((context) async { return { 'points': context.locations, 'events': context.telematics, // driving/impact events }; });除非启用
syncTelematics,否则context.telematics为空。
行程不会持久化或同步。 Tracelet.onTrip() 将已完成的行程传递给 Dart 回调是行程离开 SDK 的“唯一”方式:行程状态保存在内存中,从不写入 SQLite,也从不发送到 HTTP 端点。如果调用 start() 的隔离在行程结束之前消失,则根本不会产生行程事件 — 离线并不是丢失行程事件的唯一方式。在行程持久性发布之前,如果您需要生存,请自行将行程保存在 onTrip() 中。请参阅#356 。