App 개발 일지/Nutri Capture

Nutri Capture 프론트엔드 - NavigationBar

interfacer_han 2024. 9. 28. 17:20

#1 개요

#1-1 NavigationBar에 넣을 아이콘 가져오기

 

Material Symbols and Icons - Google Fonts

Material Symbols are our newest icons consolidating over 2,500 glyphs in a single font file with a wide range of design variants.

fonts.google.com

NavigationBar을 구현하기 위해선 먼저 아이콘이 필요하다. 위 페이지에서 SVG 아이콘을 다운로드했다. 이전 게시글에서 만든 NavHost의 Destination은 3개이므로 각각의 Destination에 어울리는 아이콘 3개를 골라 다운로드 했다 (참조: 첫번째 아이콘, 두번째 아이콘, 세번째 아이콘).

 

#1-2 NavigationBar 구현

 

[Android] Jetpack Compose - Scaffold

#1 개요#1-1 Scaffold의 사전적 의미Scaffold는 비계((건설) 높은 곳에서 공사를 할 수 있도록 임시로 설치한 가설물)라는 단어로 번역된다. #1-2 Scaffold in Jetpack Compose Jetpack Compose  |  Android Developers이

kenel.tistory.com

위 게시글의 #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

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com

 

#4-3 본 프로젝트의 가장 최신 Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com