ヘルスコネクトでワークアウト エクスペリエンスを開発する

このガイドは、ヘルスコネクトのバージョン 1.1.0 以降に対応しています。

アプリでワークアウト エクスペリエンスを構築する場合は、ヘルスコネクトを使用して次のようなことができます。

  • エクササイズ セッションを作成する
  • ワークアウトのルートを書き込む
  • 心拍数、速度、距離などのワークアウト指標を書き込む
  • 他のアプリからワークアウト データを読み取る

このガイドでは、データ型、バックグラウンド実行、権限、推奨されるワークフロー、ベスト プラクティスなど、これらのワークアウト機能の構築方法について説明します。

概要: 包括的なワークアウト トラッカーの構築

ヘルスコネクトを使用して包括的なワークアウト トラッキング エクスペリエンスを構築するには、次の主要な手順に沿って操作します。

  • Health に関する権限に基づいて権限を正しく実装している。
  • ExerciseSessionRecord を使用してセッションを録画する。
  • セッション中にワークアウト データを一貫して書き込む。
  • バックグラウンド実行を適切に管理して、継続的なデータ キャプチャを検証する。
  • ワークアウト後の概要と分析のためのセッション データの読み取り。

このワークフローにより、他のヘルスコネクト アプリとの相互運用性が実現し、ユーザーが管理するデータアクセスが検証されます。

始める前に

ワークアウト機能を実装する前に、次のことを行います。

  • 適切な依存関係を使用してヘルスコネクトを統合します。
  • HealthConnectClient インスタンスを作成します。
  • アプリがHealth Permissions に基づく実行時の権限フローを実装していることを確認します。
  • ワークフローで GPS を使用する場合は、位置情報の利用許可とフォアグラウンド サービスを設定します。

主なコンセプト

ヘルスコネクトは、いくつかのコア コンポーネントを使用してワークアウト データを表します。ExerciseSessionRecord は、ワークアウトの中心的な記録として機能し、開始時間や終了時間、エクササイズの種類などの詳細情報が含まれます。セッション中に、HeartRateRecord や SpeedRecord などのさまざまなデータ型を記録できます。屋外アクティビティの場合、ExerciseRoute は GPS データを保存し、対応するセッションにリンクします。

エクササイズ セッション

ExerciseSessionRecord は、ワークアウト データの中心的なレコードであり、1 回のワークアウト セッションを表します。各レコードには次の情報が保存されます。

  • 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 を使用して権限をリクエストします。ヘルスコネクトの権限画面が表示されます。
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 回の呼び出しですべてを保存します。

アプリがヘルスコネクトの読み取りや書き込みを行うたびに、バッテリーと処理能力がわずかに消費されるため、これは重要です。

次のコードは、バッチで記録する方法を示しています。

// 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 を使用します。これにより、特定の時点以降に挿入、更新、削除されたレコードのリストを取得できます。これは、変更を手動で追跡したり、すべてのデータを繰り返し読み取ったりするよりも効率的です。詳しくは、ヘルスコネクトとデータを同期するをご覧ください。

セッションの読み取り

エクササイズ セッションを読み取るには、タイプとして ExerciseSessionRecord を指定した ReadRecordsRequest を使用します。通常は、特定の期間でフィルタします。

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 データはエクササイズ セッションの一部として書き込まれますが、読み取りは個別に行う必要があります。セッションの ID を指定して getExerciseRoute() メソッドを使用し、ルートデータを読み取ります。

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 分間隔を目指します。
  • 書き込みリクエストをバッチ処理する: センサー イベントを個別に書き込まないでください。リクエストをチャンク化します。ヘルスコネクトは、1 回の書き込みリクエストにつき最大 1,000 件のレコードを処理します。
  • セッション ID を安定した一意のものにする: セッションには一貫した識別子を使用します。セッションが編集または更新された場合、同じ ID を使用することで、新しい別のセッションとして扱われるのを防ぐことができます。
  • データ型とルートポイントの両方にバッチ処理を使用する: 入出力のオーバーヘッドを削減し、バッテリー駆動時間を延ばすには、各ポイントを個別に書き込むのではなく、データポイントを 1 つの 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 の精度を検証する: ExerciseRoute に書き込む前に、精度の低い GPS サンプル(水平精度の半径が大きいポイントなど)を除外して、地図が整然としていてプロフェッショナルに見えることを確認します。
  • タイムスタンプが重複していないことを確認する: 前のセッションが終了する前に新しいセッションが開始されていないことを確認します。セッションが重複すると、フィットネス ダッシュボードと概要の計算で競合が発生する可能性があります。
  • 権限が必要な理由を明確に説明する: Permission.createIntent フローを使用して、アプリが健康に関するデータへのアクセスを必要とする理由を説明します(例: 「血管ストレスの傾向をモニタリングして分析情報を提供するため」)。
  • 一時停止と再開をサポートする: アプリが一時停止を正しく処理することを確認します。ユーザーが一時停止したときに、ルートポイントとデータタイプの収集を停止して、平均ペースと継続時間が正確に保たれるようにします。
  • 長時間にわたるセッションをテストする: 数時間にわたるセッション中のバッテリー消費量をモニタリングして、バッチ処理の間隔とセンサーの使用によってデバイスが消耗しないことを確認します。
  • タイムスタンプをセンサーレートに合わせる: レコードのタイムスタンプをセンサーの実際の周波数に合わせ、データの忠実度を維持します。

テスト

データの正確性と高品質のユーザー エクスペリエンスを検証するには、次のテスト戦略に沿って、公式の主なユースケースをテストするのドキュメントをご覧ください。

検証ツール

  • ヘルスコネクト ツールボックス: このコンパニオン アプリを使用して、レコードの手動検査、テストデータの削除、データベースへの変更のシミュレーションを行います。これは、レコードが正しく保存されていることを確認する最適な方法です。
  • FakeHealthConnectClient を使用した単体テスト: テスト ライブラリを使用して、権限の取り消しや API 例外などのエッジケースをアプリがどのように処理するかを、実機を使用せずに検証します。

品質チェックリスト

一般的なアーキテクチャ

ワークアウトの実装には、通常、次のものが含まれます。

コンポーネント 管理
セッション コントローラ セッションの状態
タイマー
バッチ処理ロジック
データ型コントローラ
位置情報のサンプリング
リポジトリ レイヤ(ヘルスコネクトのオペレーションをラップ) セッションを挿入する
データ型を挿入する
ルートポイントを挿入する
セッションの概要を読み取る
UI レイヤ(ディスプレイ): 時間
ライブデータの種類
地図のプレビュー
スプリットの計算
ライブ GPS トレース

トラブルシューティング

症状 考えられる原因 解決策
ルートがセッションに関連付けられていない セッション ID または期間が一致しません。 ExerciseRoute が、ExerciseSessionRecord の期間内に完全に収まる期間で記述されていることを確認します。後でセッションを参照する場合は、一貫した ID を使用していることを確認します。エクササイズのルートを記録するをご覧ください。
データタイプがない(心拍数など) 書き込み権限がないか、時間フィルタが正しくない。 特定のデータタイプの権限をリクエストし、ユーザーがそれを付与していることを確認します。ReadRecordsRequest がセッションと一致する TimeRangeFilter を使用していることを確認します。権限をご覧ください。
セッションの書き込みに失敗する タイムスタンプが重複しています。 ヘルスコネクトは、同じアプリの既存のデータと重複するレコードを拒否することがあります。新しいセッションの startTime が、前のセッションの endTime より後であることを確認してください。
GPS データが記録されていません フォアグラウンド サービスが強制終了されたか、非アクティブでした。 画面がオフのときにデータを収集するには、foregroundServiceType="health" 属性または location 属性を指定したフォアグラウンド サービスを使用する必要があります。
重複するレコードが表示される clientRecordId がありません。 各レコードの Metadata に一意の clientRecordId を割り当てます。これにより、同期の再試行中に同じデータが 2 回書き込まれた場合に、ヘルスコネクトが重複除去を実行できるようになります。ベスト プラクティスをご覧ください。

一般的なデバッグ手順

権限の状態を確認します。 読み取りまたは書き込みオペレーションを試行する前に、必ず getPermissionStatus() を呼び出してください。ユーザーはいつでもシステム設定で権限を取り消すことができます。
実行モードを確認します。 アプリがバックグラウンドでデータを収集していない場合は、AndroidManifest.xml ファイルで正しい権限を宣言していること、およびユーザーがアプリを「バッテリー制限」モードにしていないことを確認してください。