如要在應用程式中建構運動體驗,可以使用健康資料同步執行下列操作:
- 寫入運動時段
- 寫入訓練路線
- 寫入心率、速度和距離等運動指標
- 讀取其他應用程式的運動資料
本指南說明如何建構這些運動功能,涵蓋資料類型、背景執行、權限、建議的工作流程和最佳做法。
總覽:建構完整的健身追蹤器
您可以按照下列核心步驟,使用「健康資料同步」建構完整的運動追蹤體驗:
- 根據健康資料存取權正確實作權限。
- 使用
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 資訊清單中聲明該組權限。
// Create a set of permissions for required data types
val PERMISSIONS =
setOf(
HealthPermission.getReadPermission(ExerciseSessionRecord::class),
HealthPermission.getWritePermission(ExerciseSessionRecord::class),
HealthPermission.getReadPermission(ExerciseRoute::class),
HealthPermission.getWritePermission(ExerciseRoute::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 要求這些權限。系統接著會顯示 Health Connect 權限畫面。
// Create the permissions launcher
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()
val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted ->
if (granted.containsAll(PERMISSIONS)) {
// Permissions successfully granted
} else {
// Lack of required permissions
}
}
suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient) {
val granted = healthConnectClient.permissionController.getGrantedPermissions()
if (granted.containsAll(PERMISSIONS)) {
// Permissions already granted; proceed with inserting or reading data
} else {
requestPermissions.launch(PERMISSIONS)
}
}
由於使用者可以隨時授予或撤銷權限,應用程式每次使用權限前都必須先檢查,並處理權限遭撤銷的情況。
如要要求權限,請呼叫 checkPermissionsAndRun 函式:
if (!granted.containsAll(PERMISSIONS)) {
requestPermissions.launch(PERMISSIONS)
// Check if required permissions are not granted, and return
}
// 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 sessionId = UUID.randomUUID().toString()
val sessionClientId = UUID.randomUUID().toString()
val session = ExerciseSessionRecord(
id = sessionId,
exerciseType = ExerciseType.EXERCISE_TYPE_RUNNING,
startTime = Instant.now(),
endTime = null,
metadata = Metadata(clientRecordId = sessionClientId),
)
healthConnectClient.insertRecords(listOf(session))
記錄運動路線
如要進一步瞭解如何解讀指引,請參閱「讀取原始資料」。
記錄運動路線時,請批次處理資料。也就是說,您會收集一組 GPS 點,然後透過單一呼叫一次儲存所有點,而不是在每個點出現時儲存。
這點非常重要,因為應用程式每次讀取或寫入 Health Connect 時,都會耗用少許電量和處理能力。
以下程式碼顯示如何分批記錄:
// 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延遲寫入。建議間隔 15 分鐘,在即時資料和電池效率之間取得平衡。 - 批次處理:請勿個別寫入每個感應器事件。將要求分塊。每個寫入要求最多可處理 1000 筆記錄。
- 確保工作階段 ID 穩定且不重複:為工作階段使用一致的 ID。如果編輯或更新訓練記錄,使用相同 ID 可避免系統將其視為新的獨立訓練。
- 針對資料類型和路線點使用批次處理:如要減少輸入/輸出經常用量並延長電池續航力,請將資料點分組為單一
insertRecords呼叫,而非個別寫入每個點。 避免寫入重複資料:使用用戶端 ID 建立記錄時,請設定
metadata.clientRecordId。「健康資料同步」會使用這項資訊來識別不重複的記錄。如果您嘗試寫入已存在的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流程說明應用程式為何需要存取健康資料,例如「繪製跑步路線並計算卡路里燃燒量」。支援暫停和繼續:確認應用程式能正確處理暫停作業。 使用者暫停時,請停止收集路線點和資料類型,確保平均配速和時間長度維持準確。
測試長時間工作階段:監控數小時工作階段的電池耗電量,確認批次間隔和感應器用量不會耗盡裝置電力。
將時間戳記與感應器速率對齊:將記錄時間戳記與感應器的實際頻率 (例如 GPS 的 1 Hz) 相符,以維持高保真度的資料。
測試
如要驗證資料正確性並確保使用者享有優質體驗,請依循下列測試策略,並參閱官方的測試熱門用途說明文件。
驗證工具
- 健康資料同步 Toolbox:使用這款隨附應用程式手動檢查記錄、刪除測試資料,以及模擬資料庫變更。這是驗證記錄是否正確儲存的最佳方式。
- 使用
FakeHealthConnectClient進行單元測試:使用測試程式庫驗證應用程式如何處理極端情況,例如權限撤銷或 API 例外狀況,不必使用實體裝置。
品質檢查清單
一般架構
訓練實作通常包括:
| 元件 | 管理 |
|---|---|
| 工作階段控制器 | 工作階段狀態 計時器 批次處理邏輯 資料類型控制器 位置取樣 |
| 存放區層 (包裝健康資料同步作業): | 插入訓練記錄 插入資料類型 插入路線點 讀取訓練記錄摘要 |
| UI 層 (顯示): | 時間長度 即時資料類型 地圖預覽 分段計算 即時 GPS 軌跡 |
疑難排解
| 問題 | 可能原因 | 解析度 |
|---|---|---|
| 路徑未與工作階段建立關聯 | 工作階段 ID 或時間範圍不符。 | 確認 ExerciseRoute 的時間範圍完全落在 ExerciseSessionRecord 持續時間內。如要在稍後參照工作階段,請確認您使用的是一致的 ID。請參閱「記錄運動路線」。 |
| 缺少資料類型 (例如心率) | 缺少寫入權限或時間篩選器有誤。 | 確認您已要求特定資料類型權限,且使用者已授予該權限。確認 ReadRecordsRequest 使用的 TimeRangeFilter 與工作階段相符。請參閱「權限」。 |
| 工作階段無法寫入 | 時間戳記重疊。 | 如果記錄與來自同一應用程式的現有資料重疊,健康資料同步可能會拒絕。請確認新活動的 startTime晚於前一活動的 endTime。 |
| 未記錄 GPS 資料 | 前景服務已終止或無效。 | 如要在螢幕關閉時收集資料,您必須使用具有 foregroundServiceType="health" 或位置屬性的前景服務。 |
| 出現重複記錄 | 缺少 clientRecordId |
在每筆記錄的 Metadata 中指派專屬的 clientRecordId。這樣一來,如果同步重試期間寫入相同資料兩次,健康資料同步就能執行重複資料刪除作業。請參閱「最佳做法」。 |
常見的偵錯步驟
- 檢查權限狀態:嘗試讀取或寫入作業前,請務必先呼叫
getPermissionStatus()。使用者隨時可以在系統設定中撤銷權限。 - 驗證執行模式:如果應用程式未在背景收集資料,請確認您已在
AndroidManifest.xml中聲明正確的權限,且使用者未將應用程式設為「電池用量限制」模式。