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

如要在應用程式中建構運動體驗,可以使用健康資料同步執行下列操作:

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

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

總覽:建構完整的健身追蹤器

您可以按照下列核心步驟,使用「健康資料同步」建構完整的運動追蹤體驗:

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

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

事前準備

實作健身功能前,請先完成下列事項:

核心概念

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

運動課程

ExerciseSessionRecord 是運動資料的中央記錄,代表單一運動時段。每筆記錄會儲存:

  • startTime
  • endTime
  • exerciseType
  • 選填的課程中繼資料 (標題、附註)

ExerciseSessionRecord 的資料也可能包含運動路線、單圈和區間。此外,工作階段期間還可記錄其他資料類型,例如 HeartRateRecordSpeedRecord,並與工作階段建立關聯。

相關聯的資料類型

與運動訓練相關的資料會以個別記錄類型表示。常見類型包括:

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

運動路線

你可以使用 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_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 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,然後查詢在同一時間間隔內發生的 HeartRateRecordDistanceRecord

如要將運動資料與後端伺服器同步,或讓應用程式的資料儲存庫與「健康資料同步」保持一致,請使用 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
        }
    }
}

讀取資料類型

如要讀取工作階段期間的特定細微資料 (例如心率),請使用工作階段的 startTimeendTime 篩選該資料類型的要求。

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) 相符,以維持高保真度的資料。

測試

如要驗證資料正確性並確保使用者享有優質體驗,請依循下列測試策略,並參閱官方的測試熱門用途說明文件。

驗證工具

品質檢查清單

一般架構

訓練實作通常包括:

元件 管理
工作階段控制器 工作階段狀態
計時器
批次處理邏輯
資料類型控制器
位置取樣
存放區層 (包裝健康資料同步作業): 插入訓練記錄
插入資料類型
插入路線點
讀取訓練記錄摘要
UI 層 (顯示): 時間長度
即時資料類型
地圖預覽
分段計算
即時 GPS 軌跡

疑難排解

問題 可能原因 解析度
路徑未與工作階段建立關聯 工作階段 ID 或時間範圍不符。 確認 ExerciseRoute 的時間範圍完全落在 ExerciseSessionRecord 持續時間內。如要在稍後參照工作階段,請確認您使用的是一致的 ID。請參閱「記錄運動路線」。
缺少資料類型 (例如心率) 缺少寫入權限或時間篩選器有誤。 確認您已要求特定資料類型權限,且使用者已授予該權限。確認 ReadRecordsRequest 使用的 TimeRangeFilter 與工作階段相符。請參閱「權限」。
工作階段無法寫入 時間戳記重疊。 如果記錄與來自同一應用程式的現有資料重疊,健康資料同步可能會拒絕。請確認新活動的 startTime晚於前一活動的 endTime
未記錄 GPS 資料 前景服務已終止或無效。 如要在螢幕關閉時收集資料,您必須使用具有 foregroundServiceType="health" 或位置屬性的前景服務
出現重複記錄 缺少 clientRecordId 在每筆記錄的 Metadata 中指派專屬的 clientRecordId。這樣一來,如果同步重試期間寫入相同資料兩次,健康資料同步就能執行重複資料刪除作業。請參閱「最佳做法」。

常見的偵錯步驟

  • 檢查權限狀態:嘗試讀取或寫入作業前,請務必先呼叫 getPermissionStatus()。使用者隨時可以在系統設定中撤銷權限。
  • 驗證執行模式:如果應用程式未在背景收集資料,請確認您已在 AndroidManifest.xml 中聲明正確的權限,且使用者未將應用程式設為「電池用量限制」模式。