4. 移动端数据采集应用开发

移动端是DHT(Decentralized Health Trial,去中心化临床试验)远程数据采集链条中离受试者最近的一环。它要同时完成四件事:与DHT设备建立可靠通信、把采集到的数据安全缓存、在网络不稳定时保证离线可用与后续同步、以及让不同年龄和能力的受试者都能顺利完成操作。本章围绕Android与iOS两端,讲清这四件事在工程上具体怎么做。

一、与DHT设备的通信方式选择

DHT设备通常包括可穿戴传感器(心率、体温、血氧、活动量)、电子药盒、家用血压计、血糖仪、肺功能仪等。移动端与它们的通信主要有三条路径:

  • BLE(低功耗蓝牙):最常用。适合心率带、体温贴、电子药盒等低功耗设备。Android使用BluetoothGatt,iOS使用CoreBluetooth。特点是需要配对、连接、发现服务与特征值,再订阅通知或主动读取。
  • USB/串口或厂商SDK:部分血压计、血糖仪通过USB OTG或厂商提供的Android/iOS SDK接入,数据格式由厂商定义。
  • 近场或二维码中转:设备无蓝牙时,可通过设备屏幕显示二维码、手机扫码录入,或NFC触碰读取。

以BLE为例,一次完整采集的典型流程是:扫描广播 → 连接设备 → 发现服务(Service)与特征值(Characteristic)→ 订阅通知或读取 → 解析字节数组 → 写入本地缓存。下面这张图展示了移动端与DHT设备之间的信号链与数据流关系。

DHT设备 心率带 / 体温贴 电子药盒 / 血压计 GATT Server BLE广播/连接 Notify / Read 移动端App GATT连接管理 / 重连 字节解析 → 结构化数据 本地缓存(SQLite/文件) 本地缓存 待同步队列 状态标记 写入 同步引擎 网络检测 重试 / 幂等 读取 EDC 后端 API HTTPS 离线状态 无网络 → 仅写本地缓存 恢复网络 → 自动补传 网络中断 用户交互层 连接状态提示 采集进度 / 无障碍适配 反馈 图例说明 BLE通信链路 数据写入缓存 同步上传 离线降级路径 数据流向:设备 → App解析 → 本地缓存 → 同步引擎 → EDC后端;离线时缓存暂存,恢复后补传。

二、数据缓存与离线同步

移动端不能假设网络始终可用。受试者可能在地铁、电梯、偏远地区完成测量,此时数据必须先落到本地,等网络恢复后再上传。缓存设计要解决三个问题:存什么、怎么标记状态、怎么保证不丢不重。

建议每条采集记录至少包含以下字段:record_id(客户端生成的UUID)、device_id、metric_type、value、unit、measured_at(设备时间)、received_at(手机接收时间)、sync_status(pending/synced/failed)、retry_count。其中record_id是幂等键,后端以它去重,避免重复上传造成数据膨胀。

同步策略推荐“先本地后远端、批量优先、失败退避”:网络可用时按时间顺序批量提交,每批不超过50条;单条失败不阻塞整批,标记为failed并进入重试队列;重试采用指数退避(如1s、2s、4s、8s,上限5分钟),超过阈值后提示用户手动重试。Android可用WorkManager,iOS可用BGTaskScheduler,在系统允许的后台窗口内完成补传。

缓存数据库建议使用SQLite(Android Room / iOS Core Data或GRDB),并对sync_status和measured_at建索引,保证待同步队列查询和按时间排序的效率。

三、用户交互设计

DHT受试者往往不是技术人员,交互设计要降低认知负担。核心原则是:状态可见、操作可逆、错误可恢复。

  • 连接状态:用颜色和文字同时表达。例如“已连接”用绿色圆点加文字,“搜索中”用蓝色旋转图标,“连接失败”用橙色并给出“重试”按钮。不要只靠颜色区分,色盲用户无法识别。
  • 采集过程:显示实时数值或进度条,让用户知道设备正在工作。例如血压计采集时显示“正在充气…”,完成后显示收缩压/舒张压/脉搏三个数值。
  • 异常处理:设备未佩戴、电量低、信号弱时给出具体原因和下一步动作,而不是笼统的“失败”。
  • 确认与撤销:手动录入的数据在提交前允许修改;已提交但未同步的记录允许删除,删除操作写入本地日志以便审计。

四、无障碍适配

临床试验人群可能包含老年人、视障或运动功能受限者,无障碍不是可选项。Android和iOS都提供了系统级无障碍框架,工程上要落实以下几点:

适配项Android做法iOS做法
屏幕阅读器为控件设置contentDescription,自定义View实现ExploreByTouchHelper设置accessibilityLabel、accessibilityHint、accessibilityTraits
触控目标最小48dp×48dp最小44pt×44pt
动态字体使用sp单位,响应系统字体缩放使用Dynamic Type,支持UIFontMetrics
对比度正文与背景对比度≥4.5:1同左,并支持“增强对比度”开关
语音提示关键操作配合TalkBack播报配合VoiceOver播报

此外,采集流程应支持“一键完成”:对于行动不便的受试者,减少滑动、长按、多指手势,改用大按钮单击。表单输入尽量用选择器代替键盘输入,减少打字负担。

无障碍适配必须在真机上用TalkBack和VoiceOver实际走查,模拟器只能覆盖部分场景。建议在开发阶段就把无障碍检查纳入提测清单。

五、一个可参考的采集与同步闭环

以电子药盒为例:受试者打开药盒,药盒通过BLE广播“已开盖”事件;App连接后读取事件时间戳,生成一条medication_event记录,写入本地缓存并标记pending;若当前有网络,同步引擎立即上传,成功后标记synced;若无网络,记录留在队列中,App界面显示“待同步1条”,网络恢复后自动补传。整个过程用户只需打开药盒,无需额外操作。开发时可用以下伪代码验证幂等逻辑:

// 伪代码:幂等写入与同步
fun saveEvent(event: Event) {
    val existing = db.findByRecordId(event.recordId)
    if (existing != null) return  // 已存在,直接返回
    db.insert(event.copy(syncStatus = "pending"))
}

fun syncPending() {
    val batch = db.queryPending(limit = 50)
    for (e in batch) {
        try {
            api.upload(e)          // 后端以 recordId 去重
            db.markSynced(e.recordId)
        } catch (ex: Exception) {
            db.markFailed(e.recordId, retry = e.retryCount + 1)
        }
    }
}

判断同步是否成功,不能只看HTTP 200,还要核对后端返回的record_id列表与本地批次是否一致;若存在缺失,应将该条重新置为pending。只有本地队列为空且后端确认全部接收,才算完成一次完整闭环。

移动端开发的质量,最终体现在“受试者是否愿意持续使用”和“数据是否完整可信”这两件事上。通信稳定、缓存可靠、交互清晰、无障碍到位,四者缺一不可。