https://www.answertopia.com/jetpack-compose/screen-navigation-in-jetpack-compose/
https://github.com/nohjunh/Compose_SP/tree/main/Navigation
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-compose:$nav_version"
// BottomNavigation 및 BottomNavigationItem 구성요소를 사용하려면 해당 종속 항목을 추가
implementation "androidx.compose.material:material:1.4.3"
composable(
route = "${NavRoutes.Welcome.route}?userName={userName}",
arguments = listOf(navArgument("userName") {
type = NavType.StringType
defaultValue = ""
})
) { backStackEntry ->
Welcome(
navigateToProfile = navigateToProfile,
userName = backStackEntry.arguments?.getString("userName")
)
}
홈 화면을 포함해 앱을 구성하는 각 화면은 목적지(Destination)이라 불리며 하나의 컴포저블 또는 액티비티로 만들어짐.
안드로이드 네비게이션 아키텍쳐에서는 Navigation Back Stack을 이용해 앱 안에서 목적지에 이르는 사용자의 경로를 추적
처음 앱 실행 시 홈 화면이 첫 번째 목적지로 스택에 쌓이면서 스택의 맨 바닥에 놓이게 된다.
다른 목적지로 이동 시 해당 화면이 현재 목적지가 되고, 홈 목적지 위의 백 스택에 쌓임.
사용자가 시스템의 뒤로 가기 버튼을 이용해 화면을 거꾸로 이동하면, 스택에서 화면을 하나씩 꺼냄.
즉, 스택에 A -> B-> C가 쌓여 있고 현재 C 화면이 목적지라면, 뒤로가기를 눌렀을 때 해당 화면인 C를 스택에서 꺼내고, B 컴포저블 화면을 현재 목적지로 만들고 현재 화면으로 띄운다.
목적지 사이의 이동, 내비게이션 스택 관리와 관련된 모든 작업은 하나의 Navigation Controller에 의해 처리 -> 이 컨트롤러는 NavHostController 클래스에서 제공됨.
직관적인 절차에 따라 프로젝트 내에 네비게이션을 적용할 수 있는 컴포넌트
프로젝트에 네비게이션 추가 시 가장 먼저 메인에 NavHostController 인스턴스 생성
val navController = rememberNavController()
NavHostController 인스턴스 =
1. 상태 겍체이며 rememberNavController() 메소드를 호출 해 생성
2. 백 스택 관리 및 현재 목적지가 어떤 Composable인지 추적함.
-> Recomposition 하는 동안 백 스택의 무결성을 보장할 수 있음.
액티비티의 사용자 레이아웃에 추가되는 컴포넌트
사용자가 이동할 목적지의 PlaceHolder 역할을 함.
네비게이션 그래프 내에서 앱의 콘텐츠가 표시되는 지점
NavHost 호출 시 NavHostController와 NavGraph 필요.
=>
Navigation Graph = 네비게이션 컨트롤러의 컨텍스트 안에서 이동 가능한 목적지로 이용할 수 있는 모든 컴포저블로 구성됨.
이 목적지들을 경로(route) 형태로 선언
목적지 간의 경로를 정의하는 데 사용
네비게이션 그래프에 목적지 추가
Composable() 메소드를 호출하고 route와 목적지를 전달해 네비게이션에 목적지를 추가한다.
ex) 시작 목적지가 "home" 이며 3개의 목적지로 구성된 네비게이션 그래프를 포함한 NavHost
NavHost(
navController = NavController,
StartDestination = "home"
) {
Composable("home") {
Home()
}
Composable("customers") {
Customers()
}
Composable("purchases") {
Purchases()
}
}
sealed class Routes(val route: String) {
object Home: Routes("home")
object Customers: Routes("customers")
object Purchases: Routes("purchases")
}
NavHost(
navController = NavController,
StartDestination = Routes.Home.route
) {
Composable(Routes.Home.route) {
Home()
}
Composable(Routes.Customers.route) {
Customers()
}
Composable(Routes.Purchases.route) {
Purchases()
}
}
내비게이션 컨트롤러 인스턴스의 navigation() 메소드를 호출해 목적지 Composable의 경로를 지정함으로써 이동
Button(
onClick = {
navController.navigate(Routes.Customers.route)
}
) {
Text(text = "customers composable로 이동하는 Button")
}
Ex)
홈 컴포저블 -> 고객 컴포저블 -> 구매 컴포저블로 화면을 넘어가는 상황 가정
Customers 화면으로 이동하기 전에 모든 목적지를 스택에서 꺼내, 결과적으로 백 스택에 홈 목적지만 남도록 하는 예시
"Customers" 목적지로 이동하기 전에 백스택에서 "Home" 목적지까지 모든 것을 팝업합니다
Button(
onClick = {
navController.navigate(Routes.Customers.route) {
popUpTo(Routes.Home.route)
}
}
) {
Text(text = "customers composable로 이동하는 Button")
}
Button(
onClick = {
navController.navigate(Routes.Customers.route) {
popUpTo(Routes.Home.route) {
inclusive = true
}
}
}
) {
Text(text = "customers composable로 이동하는 Button")
}
현재 목적지에서 자기 자신(즉, 현재 목적지)로 또 이동하려고 하면 자신을 목적지로 하는 추가 인스턴스가 스택에 푸시된다. 대부분의 상황에서 이는 잘못된 동작이므로, 동일한 목적지의 여러 인스턴스가 스택 맨 위(최상위)에 추가되지 않도록 하려면 navigate() 메소드를 호출할 때 launchSingleTop 옵션을 true로 설정.
Button(
onClick = {
navController.navigate(Routes.Customers.route) {
launchSingleTop = true
}
}
) {
Text(text = "customers composable로 이동하는 Button")
}
한 화면에서 다른 화면으로 이동 시, 목적지에 인수를 전달하는 경우가 많음.
Compose는 한 화면에서 다른 화면으로 다양한 유형의 인수 전달을 지원하며 여러 단계를 포함.
ex)
Customers 화면에서 선택된 고객의 이름을 Customers 화면에서 Purchases 화면으로 전달해 그 고객의 구매 이력이 Purchases 화면에서 표시될 수 있도록 해야 함.
인수를 사용해 네비게이션을 하려면,
1. 목적지 경로에 인수 이름을 추가
NavHost(navController = navController, startDestination = Routes.Home.route) {
composable(Routes.Purchases.route + "/{customerName}") {
Purchases()
}
}
앱이 Purchases 목적지로의 네비게이션을 트리거 -> 인수에 할당할 값이 해당 백 스택 항목 안에 저장 -> 현재 네비게이션에 대한 백 스택 항목은 파라미터로 composable() 메서드의 후행 람다에 전달 -> 이 람다에서 추출된 파라미터를 Purchases 컴포저블에 전달하면 됨.
composable(Routes.Purchases.route + "/{customerName}") { backStackEntry ->
val customerName = backStackEntry.arguments?.getString("customerName")
Purchases(customerName)
}
기본적으로 네비게이션 인수는 String 타입으로 간주함.
다른 Type의 인수를 전달하려면 composable() 메서드의 arguments 파라미터를 통한 NavType 열거형을 이용해 타입을 지정해야 한다.
ex)
파라미터 타입을 Int으로 선언 ->
getString() 대신 getInt()를 사용하여 백 스택 항목에서 인수를 추출해야 됨.
composable(
Routes.Purchases.route + "/{customerId}",
arguments = listOf(navArgument("customerId") {
type = NavType.IntType
}
)
) { navBackStack ->
Purchases(navBackStack.arguments?.getInt("customerId"))
}
Purchases 컴포저블이 String 파라미터를 받게 됨.
@Composable
fun Purchases(customerName: String?) {
}
var selectedCustomer by remeber {
mutableStateOf("")
}
Button(
onClick = {
navController.navigate(Routes.Purchases.route + "/$selectedCustomer")
}
) {
Text(text = "Purchases composable로 이동하는 Button")
}
NavGraphBuilder = Navigation Compose 라이브러리에서 사용되는 클래스 중 하나.
NavGraphBuilder 클래스는 NavHost 내에서 NavGraph를 구성하는 데 사용됨.
그래프의 크기가 커질수록 관리가 필요함 -> 그래프를 여러 메서드로 분할할 수 있도록 함.
NavGraphBuilder 클래스를 사용하여 목적지 간의 경로를 정의하고 여러 그래프간 관계를 구성할 수 있다.
NavGraphBuilder 클래스는 목적지를 추가하고 연결하는데, navGraph() 메서드를 호출해 그래프에 목적지를 추가하며 목적지는 composable 함수와 연결됨
바텀 네비게이션 각 아이템 리스트를 클릭하면 현재 액티비티 안에서 다른 화면 사이를 이동
BottomNavigationBar의 구성요소
하나의 부모 BottomNavigationBar가 forEach 루프를 돌면서 BottomNavigationItem 자식들을 생성
각 자식 BottomNavigationItem들은 라벨과 아이콘으로 표시되고, onClick 핸들러를 통해 해당하는 목적지로 네비게이션을 수행함.
네비게이션 그래프에서는 findStartDestination() 메서드를 호출해 시작 목적지를 식별 가능
navController.graph.findStartDestination()
BottomNavigation은 launchSingleTop, saveState, restoreState를 활성화 해야 함.
각 BottomNavigationItem은 selected 프로퍼티를 통해 현재 선택되어 있는 아이템인지 전달해야 함. => 즉, BottomNavigationBar는 해당 아이템과 연결된 경로와 현재 경로 선택을 비교하는 로직이 필요
현재 경로는 NavController의 currentBackStackEntryAsState() 메서드를 통해 백 스택에 접근하고 목적지 경로 프로퍼티에 접근해 얻을 수 있음.
두 경로를 비교한 결과는 selected 프로퍼티에 할당
BottomNavigation {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
NavBarItems.BarItems.forEach { navItem ->
BottomNavigationItem(
selected = currentRoute == navItem.route
onClick = {
navController.navigate(navItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
<아이콘>
},
icon = {
<아이콘>
},
)
}
}