[핵심만 골라 배우는 젯팩 컴포즈] Navigation

노준혁·2023년 5월 7일
0

https://www.answertopia.com/jetpack-compose/screen-navigation-in-jetpack-compose/

https://github.com/nohjunh/Compose_SP/tree/main/Navigation


Dependency

    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 클래스에서 제공됨.


직관적인 절차에 따라 프로젝트 내에 네비게이션을 적용할 수 있는 컴포넌트

  • Navigation Host
  • Navigation Graph
  • Navigation Action
    이 3가지와 직접 작성하는 코드를 이용해 네비게이션 컨트롤러 인스턴스에 대한 참조를 얻고 상호작용할 수 있다.

정의

  • 개요
  1. NavHostController 클래스 생성
  2. NavHost 인스턴스에 연결
    (NavHost 인스턴스는 시작 목적지와 내비게이션 Route를 이용해 설정)
    (네비게이션 Route는 현재 액티비티에 대한 Navigation Graph를 구성)
  3. Navigation Controller의 navigate() 메서드를 호출해 네비게이션 수행
    (navigation() 메서드의 파라미터로 목적지 Composable의 Route 전달)
  4. 목적지 Composable에 람다를 통해 인수 전달도 가능

1. NavHostController

프로젝트에 네비게이션 추가 시 가장 먼저 메인에 NavHostController 인스턴스 생성

val navController = rememberNavController()

NavHostController 인스턴스 =
1. 상태 겍체이며 rememberNavController() 메소드를 호출 해 생성
2. 백 스택 관리 및 현재 목적지가 어떤 Composable인지 추적함.
-> Recomposition 하는 동안 백 스택의 무결성을 보장할 수 있음.

2. Navigation Host(=NavHost)

액티비티의 사용자 레이아웃에 추가되는 컴포넌트
사용자가 이동할 목적지의 PlaceHolder 역할을 함.
네비게이션 그래프 내에서 앱의 콘텐츠가 표시되는 지점

  • NavHost 호출 시 NavHostController와 NavGraph 필요.
    =>
    Navigation Graph = 네비게이션 컨트롤러의 컨텍스트 안에서 이동 가능한 목적지로 이용할 수 있는 모든 컴포저블로 구성됨.
    이 목적지들을 경로(route) 형태로 선언
    목적지 간의 경로를 정의하는 데 사용

  • 네비게이션 그래프에 목적지 추가
    Composable() 메소드를 호출하고 route와 목적지를 전달해 네비게이션에 목적지를 추가한다.

    • Route : 현재 네비게이션 컨트롤러의 컨텍스트 안에서 고유한 목적지를 식별할 수 있는 String 값으로 이루어짐.
    • Destination : 네비게이션을 수행할 때 호출되는 Composable
  • ex) 시작 목적지가 "home" 이며 3개의 목적지로 구성된 네비게이션 그래프를 포함한 NavHost

 NavHost(
 	navController = NavController,
    StartDestination = "home"
) {
	Composable("home") {
    	Home()
    }
    Composable("customers") {
    	Customers()
    }
    Composable("purchases") {
	    Purchases()
    }
}
    
  • 이때, 경로 문자열을 Composable() 메소드에 직접 입력하는 것 대신 sealed 클래스 이용하면 더 유연하게 경로 정의 가능
  1. 단일 위치를 이용해 경로 변경 가능
  2. 구문 검증을 함으로써 경로 오타 방지
sealed class Routes(val route: String) {
    object Home: Routes("home")
    object Customers: Routes("customers")
    object Purchases: Routes("purchases")
}
  • sealed class를 이용한 ex)
NavHost(
	navController = NavController,
    StartDestination = Routes.Home.route
) {
	Composable(Routes.Home.route) {
		Home()
	}
	Composable(Routes.Customers.route) {
		Customers()
	}
	Composable(Routes.Purchases.route) {
		Purchases()
	}
}

3. Destination 이동

내비게이션 컨트롤러 인스턴스의 navigation() 메소드를 호출해 목적지 Composable의 경로를 지정함으로써 이동

Button(
	onClick = {
		navController.navigate(Routes.Customers.route)
	}
) {
	Text(text = "customers composable로 이동하는 Button")
}
  • navigation() 메소드를 옵션을 포함하는데, 이는 후행람다로 받는다.
  1. popUpTo()
    popUpTo 네비게이션 옵션 : 해당 아이템을 스택에서 꺼내고 특정 목적지 돌아감.

Ex)
홈 컴포저블 -> 고객 컴포저블 -> 구매 컴포저블로 화면을 넘어가는 상황 가정
Customers 화면으로 이동하기 전에 모든 목적지를 스택에서 꺼내, 결과적으로 백 스택에 홈 목적지만 남도록 하는 예시

"Customers" 목적지로 이동하기 전에 백스택에서 "Home" 목적지까지 모든 것을 팝업합니다

        Button(
            onClick = {
                navController.navigate(Routes.Customers.route) {
                    popUpTo(Routes.Home.route)
                }
            }
        ) {
            Text(text = "customers composable로 이동하는 Button")
        }
  • popUpTo() 메소드도 후행람다를 통해 옵션을 포함시킬 수 있다.
    ex)
    inclusive 옵션을 사용해 네비게이션을 수행하기 전에 홈 목적지를 스택에서 꺼냄.
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")
}

4. 목적지에 인수 전달

한 화면에서 다른 화면으로 이동 시, 목적지에 인수를 전달하는 경우가 많음.
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?) {


}
  • 따라서, 결국 화면 이동을 위한
    navigate() 메서드 호출 시 인수값을 전달하기 위해서는 목적지 경로의 끝에 인수값을 추가한다.
    Purchases 화면에 전달해야 하는 값이 selectedCustomer 상태변수 저장되어 있다면,
    모든 과정이 아래 "정리"와 같이 동작한다.

정리

var selectedCustomer by remeber {
	mutableStateOf("")
}
Button(
	onClick = {
    	navController.navigate(Routes.Purchases.route + "/$selectedCustomer")
	}
) {
    Text(text = "Purchases composable로 이동하는 Button")
}
  • 버튼을 클릭하면 다음과 같은 순서로 이벤트가 발생.
  1. 현재 목적지에 대한 백 스택 항목 생성.
  2. 현재 selectedCustomer 상태변수의 값이 백 스택 항목에 저장.
  3. 백 스택 항목이 백 스택으로 푸시(추가).
  4. NavHost 선언 안에 Purchases 경로에 대한 composable() 메서드가 호출됨.
  5. composable() 메서드의 후행 람다가 백 스택 항목에서 인수 값을 추출하여 Purchases 컴포저블에 전달.

5. Navigation Builder

NavGraphBuilder = Navigation Compose 라이브러리에서 사용되는 클래스 중 하나.
NavGraphBuilder 클래스는 NavHost 내에서 NavGraph를 구성하는 데 사용됨.
그래프의 크기가 커질수록 관리가 필요함 -> 그래프를 여러 메서드로 분할할 수 있도록 함.

NavGraphBuilder 클래스를 사용하여 목적지 간의 경로를 정의하고 여러 그래프간 관계를 구성할 수 있다.

NavGraphBuilder 클래스는 목적지를 추가하고 연결하는데, navGraph() 메서드를 호출해 그래프에 목적지를 추가하며 목적지는 composable 함수와 연결됨


6. BottomNavigationBar

  • 바텀 네비게이션 각 아이템 리스트를 클릭하면 현재 액티비티 안에서 다른 화면 사이를 이동

  • BottomNavigationBar의 구성요소

  1. BottomNavigation Component
  2. BottomNavigationItem Component
  • 하나의 부모 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 = {
            	<아이콘>
            },
        )
    }
}

정리

  • 내비게이션 구현 시
  1. NavHostController 클래스 생성
  2. NavHost 인스턴스에 연결
  3. NavHost 인스턴스는 시작 목적지와 내비게이션 경로를 이용해 설정
    (내비게이션 경로는 현재 액티비티에 대한 내비게이션 그래프 구성)
  4. Navigation Controller의 navigate() 메서드를 호출해 네비게이션 수행
    (navigation() 메서드의 파라미터로 목적지 Composable의 Route 전달)
  5. 목적지 Composable에 람다를 통해 인수 전달도 가능
profile
https://github.com/nohjunh

0개의 댓글