Jetpack Compose Navigation 공부하기 2
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” 를 설명하는 완벽한 이미지가 있어 가져왔다. 출처
다른 탭에 이동할 때는 현재 탭에 쌓여 있던 스택을 다 걷어내지만 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)의 역할은 다음 두 이미지의 차이를 통해 알 수 있다.
Scaffold에 TopBar 등을 넣으면 innerPadding
값이 달라지고, 이에 따라 내부 content에 패딩이 적용되는 것으로 보인다.
마지막으로 NavHost
의 Builder
파라미터는 람다로 넣어 준다. BottomNavigation에서 사용될 composable들을 넣어 주면 된다.
여기까지의 완성물 링크