Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

🩺 Android | Health Connect 걸음 수 집계 캐시 & 상단바 최소 높이 적용

Billcorea 2025. 11. 21. 15:59
반응형

 

🩺 Android | Health Connect 걸음 수 집계 캐시 & 상단바 최소 높이 적용

앱 이미지

 

개요 (Intro)

  • 오늘의 목표: 만보계 핵심 로직(걸음 수 집계/캐시) 안정화 + 메인 화면 상단바 UI 컴팩트화
  • 배경: 기존 raw StepsRecord 합산 방식은 성능/정확도 측면 한계. TopAppBar 기본 높이 과도.
  • 해결하려는 문제: 중복 데이터 합산 리스크, 빈번한 집계 호출로 인한 UI 지연, 화면 상단 낭비 공간
  • 사용 기술: Kotlin, Jetpack Compose, Health Connect, MVVM, Coroutine, Flow
📅 날짜: 2025.11.18
🎯 목표: Health Connect 걸음 수 Aggregate + 캐시 적용 & 상단바 높이 24dp로 축소
🧰 기술: Kotlin, Android Studio, Compose, Health Connect, MVVM, Coroutines, Flow

문제 정의 (Problem / Motivation)

  • 걸음 수 계산을 raw StepsRecord 반복 합산 → 중복/성능 저하 가능성.
  • 집계를 화면 갱신마다 수행 → 불필요한 I/O 증가.
  • TopAppBar 기본 높이로 인해 실제 콘텐츠 표시 영역 감소.
  • DistanceRecord 읽기 함수에서 plus() 결과 미반영 버그 존재(기존 리스트 누적 실패).

발견된 버그 및 개선 필요 코드:

// 기존 (버그): distance 누적 실패
val rValue : List<DistanceRecord> = emptyList()
for (record in response.records) {
    rValue.plus(record) // 결과를 재할당하지 않아 누락됨
}
// 기존: Steps 합산 (중복 가능성 & 비효율)
val response = client.readRecords(ReadRecordsRequest(StepsRecord::class, ...))
var total = 0L
for (r in response.records) total += r.count

해결 과정 (How I Solved It)

  1. DistanceRecord 누락 수정: MutableList로 변환 후 add()로 확정 저장.
  2. Aggregate API 도입: StepsRecord.COUNT_TOTAL 메트릭 사용.
  3. 캐시 레이어 추가: 30초 TTL + 자정 경계 무효화. getTodaySteps(forceRefresh) 제공.
  4. 세션 종료 시간 매핑 수정: endTime 잘못된 startTime 재사용 → 실제 endTime 적용.
  5. UI 축소: TopAppBar 제거 → 24dp Box + statusBarsPadding() 로 최소 높이 상단바.
  6. CombinedUiState 도입: 여러 StateFlow를 하나로 합쳐 recomposition 감소.

핵심 적용 코드:

// Distance 누락 수정
val recordsAccum = mutableListOf<DistanceRecord>()
for (record in response.records) {
    recordsAccum.add(record)
}

// Aggregate + 캐시 (HealthConnectManager)
private data class StepCountCache(val dayEpoch: Long, val timestampMs: Long, val value: Long)
private var stepCountCache: StepCountCache? = null
private val STEP_CACHE_TTL_MS = 30_000L

private suspend fun computeTodayStepsAggregate(): Long {
    val startZdt = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS)
    val endZdt = startZdt.plusDays(1)
    val agg = healthConnectClient.aggregate(
        AggregateRequest(
            metrics = setOf(StepsRecord.COUNT_TOTAL),
            timeRangeFilter = TimeRangeFilter.between(startZdt.toInstant(), endZdt.toInstant())
        )
    )
    return agg[StepsRecord.COUNT_TOTAL] ?: 0L
}

suspend fun getTodaySteps(forceRefresh: Boolean = false): Long {
    val now = System.currentTimeMillis()
    val todayEpoch = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).toInstant().epochSecond
    val cached = stepCountCache
    val valid = cached != null && !forceRefresh &&
        cached.dayEpoch == todayEpoch && (now - cached.timestampMs) < STEP_CACHE_TTL_MS
    if (valid) return cached.value
    val fresh = computeTodayStepsAggregate()
    stepCountCache = StepCountCache(todayEpoch, now, fresh)
    return fresh
}

// ViewModel 사용
_stepsTotal.value = healthConnectManager.getTodaySteps(forceRefresh = false)

// 상단바 최소 높이 UI
Box(
  modifier = Modifier
    .fillMaxWidth()
    .height(24.dp) // status bar height target
    .background(Color(0xFF5E82FC))
    .statusBarsPadding()
) { /* Icon + Title */ }

결과 (Result)

  • 걸음 수 집계 호출 빈도 ↓ (30초 캐시로 반복 쿼리 방지).
  • UI 상단 높이 축소로 세로 가시 영역 증가.
  • 세션 종료 시간 표시 정확도 개선.
  • Distance 데이터 정상 누적 (차트/리스트 값 일치).
✅ Aggregate 기반 일일 걸음 수 정확도 상승
⚡ 집계 호출 체감 대기시간 단축 & 불필요 로딩 감소
🖼 상단바 24dp 적용으로 콘텐츠 노출 영역 확대

느낀 점 / 회고 (Reflection)

  • Health Connect Aggregate API를 우선적으로 사용하는 설계가 유지보수성과 정확성을 동시에 확보.
  • 캐시 TTL 결정(30초)은 사용자 즉각 반응성과 자원 절약 타협점으로 적절.
  • UI 영역은 초기 템플릿 의존보다 실제 사용 시나리오 측정 후 최소화하는 것이 좋음.
  • 다음 개선: WorkManager로 백그라운드 자동 재동기화, 캐시 만료 시 알림형 업데이트.

참고자료 (References)


다음 로그 예정: 백그라운드 differential changes token 주기 처리 + Room 캐시 Layer 도입.

반응형