반응형
🦾 Android | 메인 화면 뒤로가기 UX 개선, 워치/폰 걸음수 분리 표시, 설정 화면 카드화

개요 (Intro)
- 오늘의 목표 / 배경: 메인 화면의 종료 UX 개선, 워치에서 측정된 데이터의 폰 저장 및 표시 강화, 설정 화면을 카드/아이콘/설명으로 가독성 개선
- 해결하려는 문제: 뒤로가기 오작동/실수 종료 방지, 걸음수 출처(폰/워치) 혼재 표시 개선, 슬라이더 설명 부족 개선
- 사용한 기술 스택: Kotlin, Jetpack Compose, Room, Hilt, Wearable APIs
📅 날짜: 2025.12.21
🎯 목표: 뒤로가기 2회 종료, Steps/Altitude 저장 및 폰/워치 분리 표시, 설정 화면 카드화
🧰 기술: Kotlin, Android Studio, Compose, Room, Hilt, Wear OS
문제 정의 (Problem / Motivation)
- 뒤로가기 버튼 한 번만 눌러도 앱이 종료되어 실수로 종료되는 문제가 있었음.
- 워치/폰에서 들어오는 걸음수가 동일 시리즈로 묶여 출처 구분이 어려웠음.
- 설정 화면의 슬라이더/옵션에 설명이 부족해 일반 사용자에게 난이도가 높았음.
// 예시: 기존 액티비티에서 onBackPressed()로 직접 처리하던 로직
override fun onBackPressed() {
// 한 번만 눌러도 finish()로 종료 -> 실수 종료 가능성
finish()
}
해결 과정 (How I Solved It)
- 뒤로가기: Activity의 onBackPressed 커스텀 로직을 제거하고, Compose의 BackHandler로 "두 번 눌러야 종료"를 구현. 첫 눌림엔 안내 다이얼로그 표시.
- 데이터 저장: WearDataSaver와 ViewModel에서 Altitude/Steps/Location을 Room Repository에 안전 저장.
- 표시: MainScreen에서 Steps를 source("phone","wear") 기준으로 분리 집계/시각화. 중앙 StepIndicator를 폰/워치로 2개 배치.
- 설정 화면: 카드 스타일 + 아이콘 + 간략 설명/권장 범위 문자열을 strings.xml로 옮겨 i18n 대응.
// Compose에서 뒤로가기 두 번 눌러야 종료 (핵심 부분)
@Composable
fun HealthConnectApp(...) {
var backPressedOnce by remember { mutableStateOf(false) }
var showConfirmDialog by remember { mutableStateOf(false) }
// 첫 눌림 후 3초 내 재설정
LaunchedEffect(backPressedOnce) {
if (backPressedOnce) {
showConfirmDialog = true
delay(3000)
backPressedOnce = false
showConfirmDialog = false
}
}
BackHandler(enabled = true) {
if (backPressedOnce) {
// 두 번째 눌림: 전체 종료 (finishAffinity)
(LocalContext.current as? Activity)?.finishAffinity()
} else {
// 첫 눌림: 안내 다이얼로그 표시
backPressedOnce = true
showConfirmDialog = true
}
}
if (showConfirmDialog) {
AlertDialog(
onDismissRequest = { showConfirmDialog = false },
title = { Text(text = stringResource(R.string.confirm_exit_title)) },
text = { Text(text = stringResource(R.string.confirm_exit_message)) },
confirmButton = {
Button(onClick = { (LocalContext.current as? Activity)?.finishAffinity() }) {
Text(text = stringResource(R.string.exit_now))
}
},
dismissButton = {
Button(onClick = { showConfirmDialog = false }) {
Text(text = stringResource(R.string.cancel))
}
}
)
}
}
// 초보자용 주석: Steps를 폰/워치로 분리 집계하여 차트에 표시하는 로직
val recentSteps = viewModel.recentSavedSteps.collectAsState().value
val todayKey = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now())
val stepsHourlyPhone = IntArray(24) { 0 }
val stepsHourlyWear = IntArray(24) { 0 }
var stepCountPhone = 0
var stepCountWear = 0
recentSteps.filter { it.dayKey == todayKey }.forEach { step ->
// timestamp로 몇 시 데이터인지 계산
val hour = Instant.ofEpochMilli(step.timestamp).atZone(ZoneId.systemDefault()).hour
// 음수 델타 보호: 최솟값 0
val d = step.delta.toInt().coerceAtLeast(0)
if (step.source.equals("phone", ignoreCase = true)) {
stepsHourlyPhone[hour] += d
stepCountPhone += d
} else {
stepsHourlyWear[hour] += d
stepCountWear += d
}
}
// 차트 시리즈로 변환(스케일링: /10)
val seriesStepsPhone = (0..23).map { h -> stepsHourlyPhone[h] / 10f }
val seriesStepsWear = (0..23).map { h -> stepsHourlyWear[h] / 10f }
// 설정 화면 카드/아이콘/권장 범위 적용 (일부 발췌)
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Height, contentDescription = stringResource(R.string.ic_desc_altitude_fusion))
Spacer(Modifier.width(8.dp))
Text(text = stringResource(R.string.card_altitude_fusion_title), style = MaterialTheme.typography.titleMedium)
}
Text(text = stringResource(R.string.card_altitude_fusion_desc), color = Color.Gray)
Spacer(Modifier.height(8.dp))
Slider(value = altitudeFusionRatio, onValueChange = { viewModel.updateAltitudeFusionRatio(it) }, valueRange = 0f..1f)
Text(text = stringResource(R.string.rec_alt_fusion), color = Color.Gray)
}
}
결과 (Result)
- 뒤로가기 UX: 두 번 눌러야 종료되도록 안전하게 개선. 첫 눌림 시 안내 다이얼로그로 명확한 피드백 제공.
- 데이터 저장: Altitude/Steps/Location이 각각 Room Repository에 정상 저장.
- 표시: 메인 차트에 Steps(폰/워치) 분리 시리즈가 추가되고 중앙 StepIndicator도 폰/워치로 분리 표기.
- 설정 화면: 카드/아이콘/설명/권장 범위로 가독성 향상 및 국제화(strings.xml) 완비.
✅ 뒤로가기 안전 종료 구현 완료
📊 Steps 출처 분리로 분석 용이성 향상
🧭 설정 가독성 개선(카드/아이콘/권장 범위) + i18n 적용
느낀 점 / 회고 (Reflection)
- 뒤로가기 동작은 Activity와 Compose가 중복되면 혼선이 생기므로 단일 진입점(Compose BackHandler)로 관리하는 게 안정적.
- 데이터 출처(폰/워치)를 분리해 보여주니 사용자 이해도가 높아지고 디버깅도 쉬워짐.
- 설정 설명/권장 범위가 있으면 초보 사용자도 안심하고 조절할 수 있음. 차후 실제 측정 환경에 따른 동적 권장값도 고려해볼 만함.
참고자료 (References)
다음 목표: Steps 동적 권장 범위 제안, 다크 모드 대비 향상, 차트 토글(폰/워치 개별 On/Off) 추가
반응형
'모바일 앱(안드로이드)' 카테고리의 다른 글
| ⌚ Android Wear & Phone 연동 디버깅 | 고도 수집, 상태 동기화, Hilt 순환 참조 정리 (1) | 2025.12.25 |
|---|---|
| 🦾 Android/Wear | Health Connect 권한·동기화 안정화 + 걸음수 단일화 및 그래프 정리 (1) | 2025.12.23 |
| 🕹️ Android | Wear Compose UI 레이아웃 정리와 중앙정렬, 문자열 리소스화 (0) | 2025.12.17 |
| 🔍 프로젝트 진단 | Health501 아키텍처 & 코드 품질 개선 로드맵 (1) | 2025.12.15 |
| 🧪 테스트 시나리오 | AiAutoSelector 단위 테스트 실패 → 가중치 조정 및 외부 설정 리팩터링 (1) | 2025.12.13 |