이 문서는 React Native(bare) 앱에서 Android Health Connect를 통해 “오늘 소모 칼로리(kcal)” 를 가져와 화면에 표시하는 방법을 정리한 튜토리얼입니다.
핵심 요약
- 칼로리는
readRecords()만 쓰면 0이 나올 수 있어(UI에는 값이 있는데 records가 비는 케이스)aggregate()를 기본 경로로 사용합니다.- iOS(HealthKit)의
activeEnergyBurned와 의미를 맞추기 위해 Android도 Active 칼로리 우선으로 집계합니다.
gg24/android/app/build.gradle에 Health Connect client를 추가합니다.
dependencies {
implementation("androidx.health.connect:connect-client:1.2.0-alpha02")
}
gg24/android/app/src/main/AndroidManifest.xml에 다음 권한을 선언합니다.
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
그리고 Android 11+에서 Health Connect 앱 패키지 가시성을 위해 <queries>도 추가합니다.
<queries>
<package android:name="com.google.android.apps.healthdata" />
<intent>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent>
</queries>
참고: Health Connect 권한 rationale/usage 화면 intent-filter는 프로젝트의
MainActivity설정을 따르세요.
Health Connect 권한은 레코드 타입 기반으로 요청합니다.
ActiveCaloriesBurnedRecord (활동 칼로리)TotalCaloriesBurnedRecord (활동 + BMR 포함)예시:
val permissions = setOf(
HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
)
이 프로젝트에서는 권한 요청을 MainActivity.requestHealthConnectPermissions(...) 헬퍼로 처리하고, 네이티브 모듈(HealthConnectModule)에서 이를 호출합니다.
aggregate()가 필요한가?Health Connect 앱 UI에는 “오늘 칼로리”가 보이는데도, readRecords() 결과는 records.size == 0인 경우가 있습니다.
이럴 때 readRecords()만 쓰면 앱은 0 kcal로 표시됩니다.
따라서 “오늘 칼로리”는 아래 순서로 계산합니다.
aggregate()로 Active 칼로리 집계 aggregate()로 Total 칼로리 집계 readRecords()를 폴백으로 사용
AggregateRequest의 import는 아래를 사용하세요.
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import java.time.ZoneId
import java.time.ZonedDateTime
suspend fun getTodayCaloriesKcal(client: HealthConnectClient): Double {
val zoneId = ZoneId.systemDefault()
val end = ZonedDateTime.now(zoneId)
val start = end.toLocalDate().atStartOfDay(zoneId)
val range = TimeRangeFilter.between(start.toInstant(), end.toInstant())
val activeAgg = client.aggregate(
AggregateRequest(
metrics = setOf(ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL),
timeRangeFilter = range
)
)
val activeKcal =
activeAgg[ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL]?.inKilocalories ?: 0.0
val totalAgg = client.aggregate(
AggregateRequest(
metrics = setOf(TotalCaloriesBurnedRecord.ENERGY_TOTAL),
timeRangeFilter = range
)
)
val totalKcal =
totalAgg[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories ?: 0.0
if (activeKcal > 0) return activeKcal
if (totalKcal > 0) return totalKcal
// (선택) 폴백: readRecords로 합산
val activeRecords = client.readRecords(
ReadRecordsRequest(
recordType = ActiveCaloriesBurnedRecord::class,
timeRangeFilter = range
)
).records
val activeSum = activeRecords.sumOf { it.energy.inKilocalories }
if (activeSum > 0) return activeSum
val totalRecords = client.readRecords(
ReadRecordsRequest(
recordType = TotalCaloriesBurnedRecord::class,
timeRangeFilter = range
)
).records
return totalRecords.sumOf { it.energy.inKilocalories }
}
이 프로젝트는 RN ↔ 네이티브 통신을 NativeModules로 연결합니다.
<root>/src/shared/natives/Health.ts<root>/src/shared/hooks/useTodayCalories.ts<root>/src/features/home/screens/HomeScreen.tsx앱 코드에서는 보통 훅으로 감싸서 사용합니다.
const { calories, isLoading, errorMessage } = useTodayCalories();
activeEnergyBurned와 의미가 유사이 프로젝트에서는 플랫폼 지표 의미를 맞추기 위해 Active 우선으로 노출하고, 데이터가 없을 때 Total로 폴백합니다.