透過「健康資料同步」開發運動體驗

本指南適用於健康資料同步 1.1.0 以上版本。

如果您想在應用程式中建構健身體驗,可以使用 Health Connect 執行下列操作:

  • 寫入運動時段
  • 寫入訓練路線
  • 寫入心率、速度和距離等健身指標
  • 讀取其他應用程式的健身資料

本指南說明如何建構這些健身功能,涵蓋資料類型、背景執行、權限、建議的工作流程和最佳做法。

總覽:建立全方位的健身追蹤工具

您可以按照下列核心步驟,使用健康資料同步打造全方位的健身追蹤體驗:

  • 根據 Health 權限正確導入權限。
  • 使用 ExerciseSessionRecord 錄製工作階段。
  • 在工作階段期間持續寫入健身資料。
  • 妥善管理背景執行作業,確保資料持續擷取。
  • 讀取運動階段資料,提供運動後摘要和分析。

這個工作流程可與其他健康資料同步應用程式互通,並驗證使用者控管的資料存取權。

事前準備

實作健身功能前:

核心概念

「健康資料同步」會使用幾個核心元件來呈現健身資料。ExerciseSessionRecord 是運動的核心記錄,包含開始或結束時間和運動類型等詳細資料。在工作階段期間,系統可以記錄各種資料類型,例如 HeartRateRecord 或 SpeedRecord。如果是戶外活動,ExerciseRoute 會儲存 GPS 資料,並連結至對應的課程。

運動時段

ExerciseSessionRecord 是健身資料的核心記錄,代表單一健身課程。每筆記錄會儲存:

  • startTime
  • endTime
  • exerciseType
  • 選填的工作階段中繼資料 (標題、附註)

ExerciseSessionRecord 也可以包含運動路線、圈數和區隔,做為資料的一部分。此外,系統也可以在工作階段期間記錄其他資料類型 (例如 HeartRateRecord 或 SpeedRecord),並與工作階段建立關聯。

相關資料類型

與健身課程相關聯的資料會以個別記錄類型表示。常見的類型包括:

如需完整的資料類型清單,請參閱「健康資料同步資料類型」。

運動路線

您可以使用 ExerciseRoute 將路線與戶外運動建立關聯。路線由一連串 ExerciseRoute.Location 物件組成,每個物件都包含:

  • 經緯度
  • 選用高度
  • 選用方位
  • 準確度資訊
  • 時間戳記

連結工作階段路徑

ExerciseRoute 包含運動工作階段的依序位置資料。在「健康資料同步」中,這類資料不會視為獨立記錄。而是在插入或更新 ExerciseSessionRecord 時提供 ExerciseRoute 資料。

開發作業注意事項

運動追蹤應用程式通常需要長時間執行,且經常在螢幕關閉時於背景執行。建構運動功能時,請務必考量如何管理背景執行作業,並要求運動資料所需的權限。

背景執行

健身應用程式通常會在螢幕關閉時執行。在這種狀態下,您應使用:

  • 用於位置和感應器取樣的前景服務
  • WorkManager:用於延遲寫入或同步處理
  • 一般記錄寫入的批次處理策略

在所有寫入作業中保持工作階段 ID 一致,以維持連續性。

權限

應用程式必須先要求相關的健康資料同步權限,才能讀取或寫入健身資料。運動的常見權限包括運動課程、運動路線,以及心率或速度等指標。此時你應該可以執行下列操作:

  • 運動課程:ExerciseSessionRecord 的讀取和寫入權限。
  • 運動路線:ExerciseRoute 的讀取和寫入權限。
  • 心率:HeartRateRecord 的讀取和寫入權限。
  • 速度:SpeedRecord 的讀取和寫入權限。
  • 距離:DistanceRecord 的讀取和寫入權限。
  • 卡路里:TotalCaloriesBurnedRecord 的讀取和寫入權限。
  • 爬升高度:ElevationGainedRecord 的讀取和寫入權限。
  • 步頻:StepsCadenceRecord 的讀取和寫入權限。
  • Power: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 要求這些權限。系統接著會顯示 Health Connect 權限畫面。
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_RATE
  • android.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>

導入健身課程

本節說明記錄健身資料的建議工作流程。

開始上課

如要建立新的訓練活動,請按照下列步驟操作:

  1. 產生專屬工作階段 ID:確認這個 ID 穩定。如果應用程式程序終止並重新啟動,您必須能夠繼續使用相同的 ID,以免工作階段片段化。
  2. 設定 metadata.clientRecordId,避免在重試同步處理時產生重複項目。
  3. 撰寫 ExerciseSessionRecord:加入開始時間。
  4. 開始收集資料類型和 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.activelyRecorded(clientRecordId = sessionClientId, device = Device(type = Device.TYPE_PHONE)),
)

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 檔案中宣告正確的權限,且使用者未將應用程式設為「電池受限」模式。