#1 개요
#1-1 NavigationBar에 넣을 아이콘 가져오기
NavigationBar을 구현하기 위해선 먼저 아이콘이 필요하다. 위 페이지에서 SVG 아이콘을 다운로드했다. 이전 게시글에서 만든 NavHost의 Destination은 3개이므로 각각의 Destination에 어울리는 아이콘 3개를 골라 다운로드 했다 (참조: 첫번째 아이콘, 두번째 아이콘, 세번째 아이콘).
#1-2 NavigationBar 구현
위 게시글의 #4-3에서 언급했던 NavigationBar 및 NavigationItem을 구현한다.
#2 코드
#2-1 SVG 이미지를 Vector 이미지로 변경
[res] → [drawable] → [마우스 오른쪽 버튼 클릭] → [New] → [Vector Asset]에서 #1-1에서 다운로드했던 3개의 SVG 파일을 Vector 형태로 변환하여 프로젝트에 추가한다.
#2-2 Destination 정보를 담는 sealed class 선언
...
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
NutricapturenewTheme {
...
Scaffold(
...
) { innerPadding ->
// (2) NavHost 수정
NavHost(
navController = navController,
startDestination = Destination.NutrientInputScreen.route,
modifier = Modifier.padding(innerPadding)
) {
composable(route = Destination.NutrientInputScreen.route) {
NutrientInputScreen(
scope = scope,
snackbarHostState = snackbarHostState
)
}
composable(route = Destination.StatisticsScreen.route) {
StatisticsScreen(
scope = scope,
snackbarHostState = snackbarHostState
)
}
composable(route = Destination.UserInfoScreen.route) {
UserInfoScreen(
scope = scope,
snackbarHostState = snackbarHostState
)
}
}
}
}
}
}
}
// (1) sealed class 선언
sealed class Destination(val route: String, val title: String, val iconId: Int) {
data object NutrientInputScreen : Destination("nutrientInputScreen", "캡처", R.drawable.pan_tool_alt)
data object StatisticsScreen : Destination("statisticsScreen", "통계", R.drawable.stacked_line_chart)
data object UserInfoScreen : Destination("userInfoScreen", "내 정보", R.drawable.manage_accounts)
}
@Composable
fun SampleContent( ... ) { ... }
(1) sealed class 선언
3개의 Destination을 사용할 것이므로 3개의 자식 클래스를 둔다.
(2) NavHost 선언
하드 코딩해두었던 startDestination 프로퍼티의 값 그리고 각 Destination의 route 속성의 값을 (1)에서 만든 sealed class들의 값으로 수정한다.
#2-3 NavigationBar 선언 (틀 잡기)
...
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
NutricapturenewTheme {
...
Scaffold(
...
bottomBar = {
MainNavigationBar(navController)
},
...
) { innerPadding ->
...
}
}
}
}
@Composable
private fun MainNavigationBar(navController: NavHostController) {
/* TODO */
}
}
sealed class Destination(val route: String, val title: String, val iconId: Int) {
...
}
@Composable
fun SampleContent( ... ) { ... }
임시 채워넣기용으로 Scaffold의 bottomBar에 넣어주었던 BottomAppBar()를 삭제하고 그 대신 사용자 정의 컴포저블 함수 MainNavigationBar()를 넣어주었다. 이 함수에서 NavigationBar를 구현할 것이다 (TODO 주석 부분). 따라서 Navigation 동작의 주체인 NavHostController로 인수로 받게 만든다.
아래 코드는 TODO 주석 부분을 구현한 것이다.
#2-4 NavigationBar 및 NavigationBarItem 구현
// (1) NavigationBar에 들어갈 아이템 리스트
val items = listOf(
Destination.NutrientInputScreen,
Destination.StatisticsScreen,
Destination.UserInfoScreen
)
NavigationBar {
// (2) 현재 보여지고 있는 Destination 정보에 대한 Getter
val currentRoute = navController.currentDestination?.route
items.forEach { item ->
NavigationBarItem(
selected = (item.route == currentRoute),
onClick = {
navController.navigate(item.route) {
// (3) popUpTo 동작() 옵션 (설명 참조)
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// 백스택에 동일한 Destination이 혹시 있다면 그걸 재사용하게 만드는 옵션
launchSingleTop = true
// 이전에 해당 Destination에 저장했던 상태(입력했던 문자열, 스크롤 위치 등)가 있다면 불러오게 만드는 옵션
restoreState = true
}
},
icon = {
Icon(
painterResource(id = item.iconId),
contentDescription = item.title
)
}
// (4) label은 디자인적 이유로 제거함 (설명 참조)
// label = { Text(text = item.title) }
)
}
}
(1) NavigationBar에 들어갈 아이템 리스트
Destination 클래스의 자식 클래스들을 넣었다.
(2) 현재 보여지고 있는 Destination 정보에 대한 Getter
현재 Navigation이 어떤 Destination 정보를 담고 있는 지 알려주는 변수다. 이 변수는 NavigationBarItem의 selected 프로퍼티의 값을 결정할 때 사용된다. selected가 true일때, NavigationBaItem의 아이콘을 더 굵게 표시하는 식으로 활용가능한 프로퍼티다.
(3) popUpTo() 동작 옵션
NavigationBar의 아이템이 1, 2, 3으로 총 3개 있고, startDestination은 1이라고 가정한다. 현재 선택된 아이템은 3이고, 3에서 이런 저런 과정을 통해, 3', 3'', 3'''의 Destination에 위치한 상태다. 이 상태에서 NavigationBar의 아이템 2를 클릭해서 해당 Destination에 방문하려고 한다.
이때 popUpTo() 옵션이 적용된 NavController.navigate()가 수행되면 2까지 돌아가기 위해 되돌아간 경로의 모든 Destination을 백스택(이 게시글의 #2-2 참조)에서 삭제한다. 이 때, popUpTo()의 인수로 들어가는 Destination의 id값은 백스택을 어디 미만까지 삭제하는 지의 기준이 된다. 위 코드처럼 navController.graph.findStartDestination().id를 기준 삼으면 startDestination까지의 모든 Destination이 제거되는데, startDestination == 최상위 Destination이므로 최상위 Destination을 제외하고 전부 제거하겠다는 의미가 된다.
(4) label은 디자인적 이유로 제거함
#4-1에 이 label 속성을 정의해준 버전과 생략한 버전 둘 다 확인할 수 있다. 나는 아이콘만으로 Destination에 대해 충분히 직관적 설명이 가능하다고 생각한다. 따라서 생략했다. 나중에 사용자 피드백을 받아, label이 있는 편이 사용자 경험에서 공리적인 이득이 있다고 생각된다면 다시 추가하겠다.
#3 요약
NavigationBar를 만들었다.
#4 완성된 앱
#4-1 스크린샷
#4-2 이 게시글 시점의 Commit
#4-3 본 프로젝트의 가장 최신 Commit
'App 개발 일지 > Nutri Capture' 카테고리의 다른 글
Nutri Capture 프론트엔드 - NutrientScreen 구조 잡기 (1) | 2024.10.04 |
---|---|
Nutri Capture 방향성 - 고유인지감각, 새 UI 스케치 (4) | 2024.10.03 |
Nutri Capture 프론트엔드 - NavHost (1) | 2024.09.26 |
Nutri Capture 프론트엔드 - Scaffold (0) | 2024.09.25 |
Nutri Capture 방향성 - 살아남는 앱이 되려면 (0) | 2024.09.22 |