如要在應用程式中建構訓練體驗,可以使用健康資料同步執行下列操作:
- 寫入運動時段
- 寫入運動路線
- 寫入心率、速度和距離等運動指標
- 讀取其他應用程式的運動資料
本指南說明如何建構這些運動功能,涵蓋資料類型、背景執行、權限、建議的工作流程和最佳做法。
總覽:建立完整的運動追蹤表
您可以按照下列核心步驟,使用「健康資料同步」建構完整的運動追蹤體驗:
- 根據健康資料權限正確實作權限。
- 使用
ExerciseSessionRecord錄製工作階段。 - 在運動期間持續寫入運動資料。
- 妥善管理背景執行作業,驗證是否持續擷取資料。
- 讀取運動後摘要和分析資料的運動記錄資料。
這個工作流程可與其他「健康資料同步」應用程式互通,並驗證使用者控管的資料存取權。
事前準備
實作健身功能前,請先完成下列事項:
- 使用適當的依附元件整合「健康資料同步」。
- 建立
HealthConnectClient執行個體。 - 確認應用程式是否根據「健康資料授權」實作執行階段權限流程。
- 如果工作流程使用 GPS,請設定位置存取權和前景服務。
核心概念
「健康資料同步」會使用幾個核心元件來表示運動資料。ExerciseSessionRecord 是運動的中央記錄,包含開始或結束時間和運動類型等詳細資料。在運動期間,可以記錄 HeartRateRecord 或 SpeedRecord 等各種資料類型。如果是戶外活動,ExerciseRoute 會儲存 GPS 資料,並連結至相應的運動。
運動時段
ExerciseSessionRecord 是運動資料的中央記錄,代表單一運動訓練。每筆記錄都會儲存下列資訊:
startTimeendTimeexerciseType- 選填的課程中繼資料 (標題、附註)
ExerciseSessionRecord 的資料也可能包含運動路線、單圈和區間。此外,工作階段期間還可記錄其他資料類型,例如 HeartRateRecord 或 SpeedRecord,並與工作階段建立關聯。
相關聯的資料類型
與運動訓練相關的資料會以個別記錄類型表示。常見的類型包括:
HeartRateRecord:代表一系列心率測量結果。SpeedRecord:代表一系列速度測量結果。DistanceRecord:代表讀取值之間的移動距離。TotalCaloriesBurnedRecord:代表讀取值之間的總卡路里燃燒量。ElevationGainedRecord:代表讀取之間的高度變化。StepsCadenceRecord:代表讀取之間的步頻。PowerRecord:代表讀取值之間的功率輸出,常見於騎單車等活動。
如需完整的資料類型清單,請參閱「健康資料同步資料類型」。
運動路線
你可以使用 ExerciseRoute 將路線與戶外運動建立關聯。路線是由一連串的 ExerciseRoute.Location 物件組成,每個物件都包含:
- 經緯度
- 選填海拔高度
- 選填方位
- 準確度資訊
- 時間戳記
連結工作階段路徑
ExerciseRoute 包含運動期間的連續位置資料,在「健康資料同步」中不會視為獨立記錄。您可以在插入或更新 ExerciseSessionRecord 時提供 ExerciseRoute 資料。
開發作業注意事項
運動追蹤應用程式通常需要長時間執行,而且螢幕關閉時經常在背景執行。建構運動功能時,請務必考慮如何管理背景執行作業,以及要求運動資料的必要權限。
背景執行
運動應用程式通常會在螢幕關閉時執行。處於這種狀態時,您應使用:
- 用於位置和感應器取樣的前景服務
WorkManager,用於延遲寫入或同步處理- 一般記錄寫入的批次處理策略
在所有寫入作業中保持工作階段 ID 一致,確保連續性。
權限
應用程式必須先要求相關的「健康資料同步」權限,才能讀取或寫入運動資料。運動的常見權限包括運動課程、運動路線,以及心率或速度等指標。此時你應該可以執行下列操作:
- 運動時段:
ExerciseSessionRecord的讀取和寫入權限。 - 運動路線:
ExerciseRoute的讀取和寫入權限。 - 心率:
HeartRateRecord的讀取和寫入權限。 - 速度:
SpeedRecord的讀取和寫入權限。 - 距離:
DistanceRecord的讀取和寫入權限。 - 熱量:
TotalCaloriesBurnedRecord的讀取和寫入權限。 - 爬升高度:
ElevationGainedRecord的讀取和寫入權限。 - 步頻:
StepsCadenceRecord的讀取和寫入權限。 - 電源:
PowerRecord的讀取和寫入權限。 - 步驟:
StepsRecord的讀取和寫入權限。
以下範例說明如何為運動時段要求多項權限,包括路線、心率、距離、卡路里、速度和步數資料:
建立用戶端執行個體後,應用程式必須要求使用者授予權限。使用者必須能隨時授予或拒絕權限。 如要這麼做,請為所需資料類型建立一組權限。請務必先在 Android 資訊清單中聲明該組權限。
val permissions = setOf( HealthPermission.getReadPermission(ExerciseSessionRecord::class), HealthPermission.getWritePermission(ExerciseSessionRecord::class), HealthPermission.getReadPermission(HeartRateRecord::class), HealthPermission.getWritePermission(HeartRateRecord::class), HealthPermission.getReadPermission(SpeedRecord::class), HealthPermission.getWritePermission(SpeedRecord::class), HealthPermission.getReadPermission(DistanceRecord::class), HealthPermission.getWritePermission(DistanceRecord::class), HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class), HealthPermission.getWritePermission(TotalCaloriesBurnedRecord::class), HealthPermission.getReadPermission(StepsRecord::class), HealthPermission.getWritePermission(StepsRecord::class) )
getGrantedPermissions 查看應用程式是否已獲授予必要權限。如果沒有,請使用 createRequestPermissionResultContract 要求這些權限。系統會顯示「健康資料同步」權限畫面。
val permissions = setOf( HealthPermission.getReadPermission(StepsRecord::class), HealthPermission.getWritePermission(StepsRecord::class), HealthPermission.getReadPermission(HeartRateRecord::class), HealthPermission.getWritePermission(HeartRateRecord::class) ) val requestPermissionsLauncher = rememberLauncherForActivityResult( contract = PermissionController.createRequestPermissionResultContract() ) { grantedPermissions -> if (grantedPermissions.containsAll(permissions)) { coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") } } else { coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") } } }
如要要求權限,請呼叫 checkPermissionsAndRun 函式:
if (!granted.containsAll(permissions)) { // Check if required permissions are not granted, and return return emptySet() } // Permissions already granted; proceed with inserting or reading data
如果只需要要求單一資料類型 (例如心率) 的權限,請只在權限集中加入該資料類型:
存取心率資料時,系統會要求下列權限:
android.permission.health.READ_HEART_RATEandroid.permission.health.WRITE_HEART_RATE
如要為應用程式新增心率功能,請先要求 HeartRateRecord 資料類型的權限。
您需要宣告以下權限,才能寫入心率:
<application>
<uses-permission
android:name="android.permission.health.WRITE_HEART_RATE" />
...
</application>
如要讀取心率,您必須要求下列權限:
<application>
<uses-permission
android:name="android.permission.health.READ_HEART_RATE" />
...
</application>
實作健身活動
本節說明記錄運動資料的建議工作流程。
開始工作階段
如要建立新的訓練活動,請按照下列步驟操作:
- 產生專屬工作階段 ID:確認這個 ID 是否穩定。如果應用程式程序遭到終止並重新啟動,您必須能夠繼續使用相同 ID,避免工作階段中斷。
- 設定
metadata.clientRecordId,避免在重試同步時產生重複項目。 - 撰寫
ExerciseSessionRecord:加入開始時間。 - 開始收集資料類型和 GPS 資料:只有在成功初始化工作階段記錄後,才能開始收集這些資料。
範例:
val sessionClientId = UUID.randomUUID().toString() val zoneOffset = ZoneOffset.systemDefault().rules.getOffset(startTime) val session = ExerciseSessionRecord( startTime = startTime, startZoneOffset = zoneOffset, endTime = startTime.plusSeconds(3600), endZoneOffset = zoneOffset, exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, metadata = Metadata(clientRecordId = sessionClientId), ) healthConnectClient.insertRecords(listOf(session))
記錄運動路線
如要進一步瞭解如何解讀指引,請參閱「讀取原始資料」。
記錄運動路線時,請批次處理資料。也就是說,您會收集一組 GPS 點,然後在單一呼叫中一次儲存所有點,而不是在每個點出現時儲存。
這點非常重要,因為應用程式每次讀取或寫入健康資料同步時,都會耗用少許電量和處理能力。
以下程式碼顯示如何分批記錄:
// 1. Create a list to hold your route locations
val routeLocations = mutableListOf<ExerciseRoute.Location>()
// 2. Add points to your list as the exercise happens
routeLocations.add(
ExerciseRoute.Location(
time = Instant.now(),
latitude = 37.7749,
longitude = -122.4194
)
)
// ... keep adding points over a period of time ...
// 3. Save the whole list at once (Batching)
val session = ExerciseSessionRecord(
startTime = startTime,
endTime = endTime,
exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
// We pass the whole list here
exerciseRoute = ExerciseRoute(routeLocations)
)
healthConnectClient.insertRecords(listOf(session))
結束課程
停止收集資料後:
- 更新記錄:應用程式會使用
endTime更新ExerciseSessionRecord。 - 完成資料:視需要計算摘要值 (例如總距離或平均配速),並將這些值寫入為額外記錄。
val finishedSession = session.copy(endTime = Instant.now())
healthConnectClient.updateRecords(listOf(finishedSession))
讀取運動資料
應用程式可以讀取運動記錄和相關資料,藉此彙整活動、提供健康洞察資料,或將資料同步到外部伺服器。舉例來說,您可以讀取 ExerciseSessionRecord,然後查詢同一時間間隔內發生的 HeartRateRecord 或 DistanceRecord。
如要將運動資料與後端伺服器同步,或讓應用程式的資料儲存庫與健康資料同步,請使用 ChangeLogs。這樣一來,您就能擷取特定時間點之後插入、更新或刪除的記錄清單,比手動追蹤變更或重複讀取所有資料更有效率。詳情請參閱「將資料與『健康資料同步』同步」一文。
讀取工作階段
如要讀取運動時段,請使用 ReadRecordsRequest,並將 ExerciseSessionRecord 設為類型。您通常會依特定時間範圍篩選這項資料。
suspend fun readExerciseSessions(
healthConnectClient: HealthConnectClient,
startTime: Instant,
endTime: Instant
) {
val response = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = ExerciseSessionRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
)
for (exerciseRecord in response.records) {
// Process each session
val exerciseType = exerciseRecord.exerciseType
val notes = exerciseRecord.notes
}
}
讀取路線
雖然 ExerciseRoute 資料會寫入運動活動,但必須分開讀取。請使用 getExerciseRoute() 方法和活動 ID 讀取路線資料:
suspend fun readExerciseRoute(
healthConnectClient: HealthConnectClient,
exerciseSessionRecord: ExerciseSessionRecord
) {
// Check if the session has a route
val route = healthConnectClient.getExerciseRoute(
exerciseSessionRecordId = exerciseSessionRecord.metadata.id
)
when (route) {
is ExerciseRouteResponse.Success -> {
val locations = route.exerciseRoute.locations
for (location in locations) {
// Use latitude, longitude, and altitude
}
}
is ExerciseRouteResponse.NoData -> {
// Handle case where no route exists
}
is ExerciseRouteResponse.ConsentRequired -> {
// Handle case where permissions are missing
}
}
}
讀取資料類型
如要讀取工作階段期間的特定細微資料 (例如心率),請使用工作階段的 startTime 和 endTime 篩選該資料類型的要求。
suspend fun readHeartRateData(
healthConnectClient: HealthConnectClient,
exerciseSession: ExerciseSessionRecord
) {
val response = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = HeartRateRecord::class,
timeRangeFilter = TimeRangeFilter.between(
exerciseSession.startTime,
exerciseSession.endTime
)
)
)
for (heartRateRecord in response.records) {
for (sample in heartRateRecord.samples) {
val bpm = sample.beatsPerMinute
}
}
}
最佳做法
請按照下列規範操作,提升資料可靠性和使用者體驗:
- 在主動追蹤期間頻繁寫入:如果是主動追蹤,請在資料可用時寫入,或以最多 15 分鐘的間隔寫入。
- 使用 WorkManager 進行背景同步:使用
WorkManager延遲寫入。間隔時間應設為 15 分鐘,以兼顧即時資料和電池效率。 - 批次寫入要求:請勿個別寫入每個感應器事件,而是將要求分塊。健康資料同步最多可處理每個寫入要求 1000 筆記錄。
- 確保工作階段 ID 穩定且不重複:為工作階段使用一致的 ID。如果編輯或更新工作階段,使用相同 ID 可避免系統將其視為新的獨立工作階段。
- 針對資料類型和路徑點使用批次處理:如要減少輸入/輸出經常用量並延長電池續航力,請將資料點分組為單一
insertRecords呼叫,而非個別寫入每個點。 - 避免寫入重複資料:使用用戶端 ID:建立記錄時,請設定
metadata.clientRecordId。健康資料同步會使用這個 ID 識別不重複的記錄。如果您嘗試寫入的記錄已存在clientRecordId,健康資料同步會忽略重複的記錄或更新現有記錄,而不是建立新記錄。設定metadata.clientRecordId是在同步重試或重新安裝應用程式時,防止重複資料的最佳方法。val record = StepsRecord( count = 100, startTime = startTime, endTime = endTime, startZoneOffset = ZoneOffset.UTC, endZoneOffset = ZoneOffset.UTC, metadata = Metadata( // Use a unique ID from your own database clientRecordId = "daily_steps_2023_10_27_user_123" ) )
- 檢查現有資料:同步處理前,請查詢時間範圍,確認應用程式中是否已有記錄。
- 驗證 GPS 準確度:先篩除低準確度的 GPS 樣本 (例如水平準確度半徑較大的點),再寫入
ExerciseRoute,確認地圖看起來乾淨且專業。 - 確認時間戳記不會重疊:確認新工作階段不會在前一個工作階段結束前開始。如果運動記錄重疊,健身資訊主頁和摘要計算可能會發生衝突。
- 清楚說明權限要求理由:使用
Permission.createIntent流程說明應用程式需要存取健康資料的原因,例如:「監控血壓趨勢並提供深入分析」。 - 支援暫停和繼續:確認應用程式能正確處理暫停狀態。使用者暫停時,請停止收集路線點和資料類型,確保平均配速和時間長度維持準確。
- 測試長時間工作階段:監控持續數小時的工作階段期間的電池耗電量,確認批次間隔和感應器用量不會耗盡裝置電力。
- 根據感應器速率調整時間戳記:將記錄時間戳記與感應器的實際頻率相符,確保資料準確度。
測試
如要驗證資料正確性並確保使用者體驗優質,請遵循下列測試策略,並參閱官方的測試熱門用途說明文件。
驗證工具
- 健康資料同步 Toolbox:使用這款隨附應用程式手動檢查記錄、刪除測試資料,以及模擬資料庫變更。這是驗證記錄是否正確儲存的最佳方式。
- 使用
FakeHealthConnectClient進行單元測試:使用測試程式庫驗證應用程式如何處理極端情況,例如權限撤銷或 API 例外狀況,不必使用實體裝置。
品質檢查清單
一般架構
訓練實作通常包括:
| 元件 | 管理 |
|---|---|
| 工作階段控制器 | 工作階段狀態 計時器 批次處理邏輯 資料類型控制器 位置取樣 |
| 存放區層 (包裝健康資料同步作業): | 插入訓練記錄 插入資料類型 插入路線點 讀取訓練記錄摘要 |
| UI 層 (顯示): | 時間長度 即時資料類型 地圖預覽 分段計算 即時 GPS 軌跡 |
疑難排解
| 問題 | 可能原因 | 解析度 |
|---|---|---|
| 路徑未與工作階段建立關聯 | 工作階段 ID 或時間範圍不符。 | 確認 ExerciseRoute 的時間範圍完全落在 ExerciseSessionRecord 持續時間內。如要在稍後參照工作階段,請確認您使用的是一致的 ID。請參閱「記錄運動路線」。 |
| 缺少資料類型 (例如心率) | 缺少寫入權限或時間篩選器有誤。 | 確認您已要求特定資料類型權限,且使用者已授予權限。確認 ReadRecordsRequest 使用的 TimeRangeFilter 與工作階段相符。請參閱「權限」。 |
| 工作階段無法寫入 | 時間戳記重疊。 | 如果記錄與來自同一應用程式的現有資料重疊,健康資料同步可能會拒絕。請確認新運動的startTime晚於前一項運動的endTime。 |
| 未記錄 GPS 資料 | 前景服務已終止或處於非使用中狀態。 | 如要在螢幕關閉時收集資料,您必須使用具有 foregroundServiceType="health" 或位置資訊屬性的前景服務。 |
| 出現重複記錄 | 缺少 clientRecordId。 |
在每筆記錄的 Metadata 中指派專屬的 clientRecordId。這樣一來,如果同步重試期間寫入相同資料兩次,健康資料同步就能執行重複資料刪除作業。請參閱「最佳做法」。 |
常見的偵錯步驟
| 檢查權限狀態。 | 請務必先呼叫 getPermissionStatus(),再嘗試讀取或寫入作業。使用者隨時可以在系統設定中撤銷權限。 |
| 確認執行模式。 | 如果應用程式未在背景收集資料,請確認您已在 AndroidManifest.xml 檔案中聲明正確的權限,且使用者未將應用程式設為「電池用量限制」模式。 |