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

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

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

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

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

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

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

このワークフローにより、他のヘルスコネクト アプリとの相互運用が可能になり、ユーザーが制御するデータアクセスが検証されます。

始める前に

ワークアウト機能を実装する前に:

主な概念

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

エクササイズ セッション

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

  • startTime
  • endTime
  • exerciseType
  • 省略可能なセッション メタデータ(タイトル、メモ)

ExerciseSessionRecord には、データの一部としてエクササイズ ルート、ラップ、セグメントを含めることもできます。また、セッション中に HeartRateRecordSpeedRecord などの他のデータ型を記録して、セッションに関連付けることもできます。

関連付けられたデータ型

ワークアウト セッションに関連付けられたデータは、個々のレコードタイプで表されます。一般的なタイプは次のとおりです。

データ型の完全なリストについては、ヘルスコネクトのデータ型をご覧ください。

エクササイズのルート

ExerciseRoute を使用して、ルートを屋外ワークアウトに関連付けることができます。ルートは、次の要素を含む ExerciseRoute.Location オブジェクトのシーケンスで構成されます。

  • 緯度と経度
  • 高度(省略可)
  • オプションの方位
  • 精度情報
  • タイムスタンプ

セッション ルートをリンクする

ExerciseRoute には、エクササイズ セッションの連続した位置情報が含まれます。ヘルスコネクトでは独立したレコードとして扱われません。代わりに、ExerciseSessionRecord の挿入時または更新時に ExerciseRoute データを提供します。

開発時の考慮事項

ワークアウト トラッキング アプリは、長時間実行する必要があることが多く、画面がオフのときはバックグラウンドで実行されることがよくあります。ワークアウト機能を構築する際は、バックグラウンド実行を管理し、ワークアウト データに必要な権限をリクエストする方法を検討することが重要です。

バックグラウンド実行

ワークアウト アプリは、画面をオフにした状態で実行されることがよくあります。この状態では、次のものを使用する必要があります。

  • 位置情報とセンサーのサンプリングのフォアグラウンド サービス
  • 遅延書き込みまたは同期の場合は WorkManager
  • 通常のレコード書き込みのバッチ処理戦略

すべての書き込みでセッション ID を一貫して維持することで、継続性を維持します。

権限

アプリは、ワークアウト データの読み取りまたは書き込みを行う前に、関連するヘルスコネクトの権限をリクエストする必要があります。ワークアウトの一般的な権限には、エクササイズ セッション、エクササイズ ルート、心拍数や速度などの指標が含まれます。以下のようなデータが収集されます。

  • エクササイズ セッション: ExerciseSessionRecord の読み取り権限と書き込み権限。
  • エクササイズのルート: ExerciseRoute の読み取り権限と書き込み権限。
  • 心拍数: HeartRateRecord の読み取り権限と書き込み権限。
  • 速度: SpeedRecord の読み取りと書き込みの権限。
  • 距離: DistanceRecord の読み取り権限と書き込み権限。
  • カロリー: TotalCaloriesBurnedRecord の読み取りと書き込みの権限。
  • 獲得標高: ElevationGainedRecord の読み取り / 書き込み権限。
  • 歩数ケイデンス: StepsCadenceRecord の読み取り権限と書き込み権限。
  • Power: 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 を使用して権限をリクエストします。ヘルスコネクトの権限画面が表示されます。

// 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 の各ポイントを発生するたびに保存するのではなく、ポイントのグループを収集して、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))

セッションを終了する

データ収集を停止した後:

  • レコードを更新する: アプリが endTimeExerciseSessionRecord を更新します。
  • データを確定する: 必要に応じて、合計距離や平均ペースなどの要約値を計算し、追加のレコードとして書き込みます。
val finishedSession = session.copy(endTime = Instant.now())
healthConnectClient.updateRecords(listOf(finishedSession))

ワークアウト データの読み取り

アプリは、エクササイズ セッションとその関連データを読み取って、アクティビティを要約したり、健康に関する分析情報を提供したり、外部サーバーとデータを同期したりできます。たとえば、ExerciseSessionRecord を読み取り、同じ時間間隔で発生した HeartRateRecord または DistanceRecord をクエリできます。

ワークアウト データをバックエンド サーバーと同期する必要がある場合や、アプリのデータストアをヘルスコネクトで最新の状態に保つ必要がある場合は、ChangeLog を使用します。これにより、特定の時点以降に挿入、更新、削除されたレコードのリストを取得できます。これは、変更を手動で追跡したり、すべてのデータを繰り返し読み取ったりするよりも効率的です。詳しくは、ヘルスコネクトとデータを同期するをご覧ください。

セッションの読み取り

エクササイズ セッションを読み取るには、型として 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
        }
    }
}

読み取りデータ型

セッション中に発生した特定の詳細データ(心拍数など)を読み取るには、セッションの 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 分間隔を目指します。
    • バッチ処理: 個々のセンサー イベントを個別に書き込まないでください。リクエストをチャンクします。ヘルスコネクトは、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 フローを使用して、アプリが健康データにアクセスする必要がある理由(「ランニングを地図に表示して消費カロリーを計算するため」など)を説明します。

  • 一時停止と再開をサポート: アプリが一時停止を正しく処理することを確認します。ユーザーが一時停止した場合は、ルートポイントとデータ型の収集を停止して、平均ペースと所要時間の精度を維持します。

  • 長時間実行されるセッションをテストする: 数時間続くセッション中のバッテリー消費量をモニタリングして、バッチ処理間隔とセンサーの使用量がデバイスのバッテリーを消耗させないことを確認します。

  • タイムスタンプをセンサーのレートに合わせる: データの忠実度を維持するため、記録のタイムスタンプをセンサーの実際の頻度(GPS の場合は 1 Hz など)に合わせます。

テスト

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

確認ツール

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

品質チェックリスト

一般的なアーキテクチャ

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

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

トラブルシューティング

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

一般的なデバッグ手順

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