앱에서 운동 환경을 빌드하려는 경우 헬스 커넥트를 사용하여 다음 작업을 할 수 있습니다.
- 운동 세션 작성
- 운동 경로 쓰기
- 심박수, 속도, 거리와 같은 운동 수치 쓰기
- 다른 앱의 운동 데이터 읽기
이 가이드에서는 데이터 유형, 백그라운드 실행, 권한, 권장 워크플로, 권장사항을 다루면서 이러한 운동 기능을 빌드하는 방법을 설명합니다.
개요: 포괄적인 운동 추적기 빌드
다음 핵심 단계를 따라 헬스 커넥트를 사용하여 포괄적인 운동 추적 환경을 빌드할 수 있습니다.
- 건강 권한에 따라 권한을 올바르게 구현합니다.
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를 사용하여 이러한 권한을 요청합니다. 헬스 커넥트 권한 화면이 표시됩니다.
// 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 포인트가 발생할 때마다 저장하는 대신 포인트 그룹을 수집하고 단일 호출에서 한 번에 모두 저장합니다.
앱이 헬스 커넥트에서 읽거나 쓸 때마다 약간의 배터리와 처리 능력을 사용하므로 이는 중요합니다.
다음 코드는 일괄 기록하는 방법을 보여줍니다.
// 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를 쿼리할 수 있습니다.
운동 데이터를 백엔드 서버와 동기화하거나 앱의 데이터 스토어를 헬스 커넥트와 최신 상태로 유지해야 하는 경우 변경 로그를 사용하세요. 이를 통해 특정 시점 이후에 삽입, 업데이트 또는 삭제된 레코드 목록을 가져올 수 있으므로 변경사항을 수동으로 추적하거나 모든 데이터를 반복적으로 읽는 것보다 효율적입니다. 자세한 내용은 헬스 커넥트로 데이터 동기화를 참고하세요.
세션 읽기
운동 세션을 읽으려면 유형이 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를 사용합니다. 실시간 데이터와 배터리 효율성 간의 균형을 맞추려면 15분 간격으로 설정하세요. - 일괄 처리: 모든 단일 센서 이벤트를 개별적으로 작성하지 마세요. 요청을 청크로 나눕니다. 헬스 커넥트는 쓰기 요청당 최대 1,000개의 레코드를 처리합니다.
- 세션 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 정확도 검증:
ExerciseRoute에 쓰기 전에 정확도가 낮은 GPS 샘플 (예: 수평 정확도 반경이 큰 포인트)을 필터링하여 지도가 깔끔하고 전문적으로 보이는지 확인합니다.타임스탬프가 중복되지 않도록 함: 이전 세션이 종료되기 전에 새 세션이 시작되지 않는지 확인합니다. 세션이 중복되면 피트니스 대시보드와 요약 계산에 충돌이 발생할 수 있습니다.
권한에 대한 명확한 근거 제공:
Permission.createIntent흐름을 사용하여 앱에 건강 데이터 액세스가 필요한 이유를 설명합니다(예: '러닝을 매핑하고 칼로리 소모량을 계산하기 위해').일시중지 및 재개 지원: 앱이 일시중지를 올바르게 처리하는지 확인합니다. 사용자가 일시중지하면 평균 페이스와 시간이 정확하게 유지되도록 경로 포인트와 데이터 유형의 수집을 중지합니다.
장기 실행 세션 테스트: 여러 시간 동안 지속되는 세션 중에 배터리 소모를 모니터링하여 일괄 처리 간격과 센서 사용이 기기를 소모하지 않는지 확인합니다.
타임스탬프를 센서 속도와 정렬: 데이터 충실도를 유지하려면 기록 타임스탬프를 센서의 실제 빈도 (예: GPS의 경우 1Hz)와 일치시킵니다.
테스트
데이터 정확성과 고품질 사용자 환경을 확인하려면 다음 테스트 전략을 따르고 공식 주요 사용 사례 테스트 문서를 참고하세요.
인증 도구
- 헬스 커넥트 도구 상자: 이 컴패니언 앱을 사용하여 레코드를 수동으로 검사하고, 테스트 데이터를 삭제하고, 데이터베이스의 변경사항을 시뮬레이션합니다. 레코드가 올바르게 저장되고 있는지 확인하는 가장 좋은 방법입니다.
FakeHealthConnectClient를 사용한 단위 테스트: 테스트 라이브러리를 사용하여 실제 기기 없이 권한 취소나 API 예외와 같은 극단적인 사례를 앱에서 처리하는 방식을 확인합니다.
품질 체크리스트
일반적인 아키텍처
운동 구현에는 일반적으로 다음이 포함됩니다.
| 구성요소 | 관리 |
|---|---|
| 세션 컨트롤러 | 세션 상태 타이머 일괄 처리 로직 데이터 유형 컨트롤러 위치 샘플링 |
| 저장소 레이어 (헬스 커넥트 작업을 래핑함) | 세션 삽입 데이터 유형 삽입 경로 포인트 삽입 세션 요약 읽기 |
| UI 레이어 (디스플레이): | 지속 시간 실시간 데이터 유형 지도 미리보기 스플릿 계산 실시간 GPS 추적 |
문제 해결
| 증상 | 가능한 원인 | 해상도 |
|---|---|---|
| 경로가 세션과 연결되지 않음 | 세션 ID 또는 시간 범위가 일치하지 않습니다. | ExerciseRoute이 ExerciseSessionRecord 기간 내에 완전히 포함되는 시간 범위로 작성되었는지 확인합니다. 나중에 세션을 참조하는 경우 일관된 ID를 사용하고 있는지 확인합니다. 운동 경로 기록하기를 참고하세요. |
| 데이터 유형 누락 (예: 심박수) | 쓰기 권한이 누락되었거나 시간 필터가 잘못되었습니다. | 특정 데이터 유형 권한을 요청했고 사용자가 이를 부여했는지 확인합니다. ReadRecordsRequest이 세션과 일치하는 TimeRangeFilter을 사용하는지 확인합니다. 권한을 참고하세요. |
| 세션 쓰기 실패 | 타임스탬프가 중복됩니다. | 헬스 커넥트에서 동일한 앱의 기존 데이터와 중복되는 레코드를 거부할 수 있습니다. 새 세션의 startTime이 이전 세션의 endTime보다 늦은지 확인하세요. |
| GPS 데이터가 기록되지 않음 | 포그라운드 서비스가 종료되었거나 비활성 상태입니다. | 화면이 꺼져 있는 동안 데이터를 수집하려면 foregroundServiceType="health" 또는 위치 속성이 있는 포그라운드 서비스를 사용해야 합니다. |
| 중복 레코드가 표시됨 | clientRecordId 누락 |
각 레코드의 Metadata에 고유한 clientRecordId를 할당합니다. 이렇게 하면 동기화 재시도 중에 동일한 데이터가 두 번 쓰여진 경우 헬스 커넥트에서 중복 삭제를 실행할 수 있습니다. 권장사항을 참고하세요. |
일반적인 디버깅 단계
- 권한 상태 확인: 읽기 또는 쓰기 작업을 시도하기 전에 항상
getPermissionStatus()를 호출합니다. 사용자는 언제든지 시스템 설정에서 권한을 취소할 수 있습니다. - 실행 모드 확인: 앱이 백그라운드에서 데이터를 수집하지 않는 경우
AndroidManifest.xml에서 올바른 권한을 선언했는지, 사용자가 앱을 '배터리 제한' 모드로 설정하지 않았는지 확인합니다.