3 minute read

Jetpack Compose Navigation 공부하기 2

이번에는 1. 인수를 통해 이동, 2. 중첩 탐색, 3. 하단 탐색 메뉴와 통합 기능을 공부해 본다.

1. 인수를 통해 이동

기존에는 NavHost를 만들 때 startDestination에 문자열로 된 경로의 이름를 넣어 주었다.

    NavHost(
        navController = navController,
        startDestination = "book/{bookName}/{bookPage}"
    ) {
        composable(
            route = "book/{bookName}/{bookPage}",
            arguments = listOf(
                navArgument("bookName") {
                    type = NavType.StringType
                    defaultValue = "default"
                },
                navArgument("bookPage") {
                    type = NavType.IntType
                    defaultValue = 1
                },
            )
        ) { backStackEntry ->
            Book(navController, bookName = backStackEntry.arguments?.getString("bookName"))
        }
    }
}

인수를 사용하려면 위와 같은 형태로 만들어 준다. 실제로 “profile/JuTaK97/35”로 navigate하려면 다음과 같이 써 주면 된다.

val bookName = "JuTaK97"
val pageNum = 35
navController.navigate("book/$bookName/$pageNum")

1-1. 선택적 인수

쿼리 매개변수 구문 형태로 선택적 인수도 보낼 수 있다. 이떄는 꼭 defaultValue를 설정해줘야 한다.

    NavHost(
        navController = navController,
        startDestination = "book?bookName={bookName}&bookPage={bookPage}"
    ) {
        composable(
            route = "book?bookName={bookName}&bookPage={bookPage}",
            arguments = listOf(
                navArgument("bookName") {
                    type = NavType.StringType
                    defaultValue = "default"
                },
                navArgument("bookPage") {
                    type = NavType.IntType
                    defaultValue = 35
                },
            )
        ) { backStackEntry ->
            val bookName = backStackEntry.arguments?.getString("bookName")
            val bookPage = backStackEntry.arguments?.getInt("bookPage")
            
            Book(navController, bookName = bookName, bookPage = bookPage)
        }
    }

실제로 navigate할 때는 다음과 같이 써 주면 된다.

val bookName = "JuTaK97"
val pageNum = 35
navController.navigate("book?bookName=$bookName&pageNum=$pageNum")

만약에 까먹고 인수 하나를 쓰지 않았더라도 설정해 놓은 default 값대로 이동하게 된다.

2. 중첩 그래프 사용하기

NavGraph가 방대해지면 모듈화 해서 관리하는 것이 편하다. 또는 어플리케이션 내에서 독립적인 흐름(예: 로그인)은 중첩 그래프로 모듈화하는 것이 관리하기 편할 것이다.
메인 화면의 버튼을 눌러 진입할 수 있는 돌고 도는 화면들을 만들어 보려고 한다.

fun NavGraphBuilder.roundRound(navController: NavController) {
    navigation(startDestination = "round1", route = "round") {
        composable("round1") { Round1(navController) }
        composable("round2") { Round2(navController) }
        composable("round3") { Round3(navController) }
    }
}

이렇게 만든 확장 함수 roundRound는 composable()들과 같은 곳에 설정해 두면 된다.

NavHost(navController, "home") {
    composable(~~~~)
    composable(~~~~)
    
    roundRound(navController) // 이곳
}

캡슐화된 그래프는 “route”라는 경로로 진입할 수 있다. 다른 곳에서 `navController.navigate(“route”)를 하면 이 중첩 그래프로 들어오게 되고 “round1”이 시작하는 곳이 된다.

@Composable
fun Round1(navController: NavController) {
    Column {
        Text(text = "Here is Round 1", fontSize = 30.sp)
        Button(onClick = { navController.navigate("round2") }) {
            Text(text = "Go to Round 2", fontSize = 25.sp)
        }
    }
}

각 Round라는 페이지는 이렇게 만들어서 1->2->3->1로 빙글빙글 돌도록 만들어 보았다.

3. 하단 탐색 메뉴

Scaffold는 여러 레이아웃 구조를 모아서 구성할수 있게 해 주는 material design 레이아웃이다. TopBar, Drawer 등 여러 구성 요소가 있지만 그 중에서 BottomNavigation을 사용해 본다.

    val bottomItems = listOf(
        "home",
        "friendslist",
        "setting"
    )

    Scaffold(
        bottomBar = {
            BottomNavigation {
                // TODO
            }
        }
    ) { innerPadding ->
        // TODO
    }

기본 구조는 위와 같다. 먼저 BottomNavigation() 의 내부부터 살펴본다.

    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination
    bottomItems.forEach { itemName ->
        BottomNavigationItem(
            icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
            label = { Text(itemName) },
            selected = currentDestination?.hierarchy?.any { it.route == itemName } == true,
            onClick = {
                navController.navigate(itemName) {
                    popUpTo(navController.graph.findStartDestination().id) {
                        saveState = true
                    }
                    launchSingleTop = true
                    restoreState = true
                }
            }
        )
    }

현재 선택되어 있는 탭을 알기 위해 navController.currentBackStackEntryAsState()?.destination?.currentDestination?.hierarchy를 사용한다. 현재 destination부터 시작해서 스택 내의 parent들을 Sequence 자료형으로 가져올 수 있고, .any {} 를 사용해서 이 경로 내에 itemName과 일치하는 게 있다면 selected가 true가 된다.
onClick에서는 navController.navigate()를 사용해서 하단 아이콘을 눌렀을 때 이동하도록 한다. 사용자가 탭을 반복해서 이동할 때 스택이 무한정 쌓이면 좋지 않으므로 popUpTo()를 이용해서 다 걷어내고 스택에 쌓이도록 한다. saveState와 `restoreState” 를 설명하는 완벽한 이미지가 있어 가져왔다. 출처 image

다른 탭에 이동할 때는 현재 탭에 쌓여 있던 스택을 다 걷어내지만 saveState가 true이므로 스택을 저장해 놓는다. 그리고 다른 탭에 갔다가 돌아 왔을 때, restoreState가 true이므로 이를 복구해 준다. 유튜브에서 ‘보관함’ 탭에서 재생 기록을 보던 중에 ‘홈’ 탭으로 이동했다가 돌아오더라도 재생 기록 화면이 복구되는 것을 생각하면 될 것 같다.

Scaffold의 bottomBar 파라미터는 이렇게 채웠고, 마지막 파라미터인 content는 람다로 뒤에 넣어 준다.

    ) { innerPadding ->
        NavHost(
            navController,
            startDestination = "home",
            Modifier.padding(innerPadding)
        ) {
            main(navController)
            composable("friendslist") { FriendList(navController) }
            composable("setting") { SettingPage(navController) }
        }
    }

Modifier.padding(innerPadding)의 역할은 다음 두 이미지의 차이를 통해 알 수 있다. image

Scaffold에 TopBar 등을 넣으면 innerPadding 값이 달라지고, 이에 따라 내부 content에 패딩이 적용되는 것으로 보인다.

마지막으로 NavHostBuilder 파라미터는 람다로 넣어 준다. BottomNavigation에서 사용될 composable들을 넣어 주면 된다.

여기까지의 완성물 링크