Compose Navigation

노준혁·2023년 4월 1일
0

https://developer.android.com/jetpack/compose/navigation?hl=ko



  • Key Point
    • State Hoisting : 상태를 관리하는 코드를 상위 컴포넌트로 옮겨서 상태를 공유하거나 전달하는 방식
      • 자식 컴포넌트에서 상태를 변경하더라도 부모 컴포넌트에서 관리하고 있는 상태를 변경할 수 있으며, 상위 컴포넌트에서 상태를 공유 관리 가능
      • 하위 컴포넌트에서 폼에 입력한 값을 저장하고, 부모 컴포넌트에서 이 값을 사용해야 할 경우, 상태 호이스팅을 사용하여 하위 컴포넌트에서 입력값을 상위 컴포넌트로 전달 -> 상위 컴포넌트에서 폼에 입력한 값을 저장하고, 이 값을 다른 하위 컴포넌트에서 사용할 수 있수도 있음.
      • 상태 호이스팅은 코드의 유지보수성을 높이고, 컴포넌트 간의 결합도를 낮출 수 있어서 코드의 재사용성을 높이는 등의 장점
    • NavHost
      Navigation 구성 요소에서 Navigation Graph와 NavController를 연결하는 역할
      NavHost는 최상위 Composable 함수 내에서 사용되며, NavController와 함께 NavGraph를 인자로 받음.
      NavHost는 NavGraph에서 정의된 대로 다양한 화면 전환을 처리하고, NavController를 통해 각각의 화면 간 전환을 관리.
      'NavHost'는 네비게이션 그래프를 가지며, 이 그래프에는 여러 목적지가 존재
      'startDestination'은 네비게이션 그래프의 최상위 목적지를 설정함
  • NavController
    NavController는 Navigation 구성 요소의 중심 API로, 각각의 화면에서 NavHost와 연결되어 사용됨. NavController는 앱의 화면 전환을 추적하는데 사용되며, 각각의 화면에서 이전 화면으로 돌아갈 수 있는 백 스택을 관리. 또한, NavController는 앱 내에서 다른 화면으로 전환하거나, 앱 내에서 데이터를 공유하는 등의 기능을 수행.

  • NavGraph
    NavGraph는 Navigation 구성 요소에서 사용되는 화면 전환과 앱 내에서 데이터를 공유하기 위한 그래프를 정의하는 XML 파일. NavGraph는 앱의 모든 화면과 화면 간 전환을 정의하며, 화면 간 전환 시에 NavHost와 NavController를 사용하여 관리됩니다. 각각의 화면은 Composable 함수로 정의되며, NavGraph에서는 Composable 함수와 그 함수가 표현하는 화면을 연결.


  • NavControllerNavigation
    구성요소의 중심 API로, 스테이트풀(Stateful)이며 앱의 화면과 각 화면 상태를 구성하는 컴포저블의 백 스택을 추적함.

  • Composable back stack : Navigation 아키텍처 구성요소에서 사용되는 개념
    (화면 전환 시에 이전 화면 상태를 유지하기 위해 사용됨.)

Navigation 구성 요소에서 각각의 화면은 Navigation Graph에 정의되어 있으며, NavController는 이러한 화면 간의 전환을 관리.

Composable back stack은 이전 화면의 상태를 추적하며, 이전 화면으로 돌아갈 때마다 해당 상태를 다시 활성화. -> 앱의 UX를 개선하고, 사용자가 이전 화면으로 쉽게 돌아갈 수 있도록 하며 이전 화면으로 돌아가는 과정에서 부드러운 화면 전환이 가능

컴포저블에서 rememberNavController() 메서드를 사용하여 NavController 생성

val navController = rememberNavController()

단, 컴포저블 계층 구조에서 NavController를 만드는 위치는 이를 참조해야 하는 모든 컴포저블이 액세스할 수 있는 곳이어야 함.
이는 상태 호이스팅(State Hoisting)의 원칙을 준수하며, 이렇게 하면 NavController와 상태를 사용할 수 있다.
상태는 currentBackStackEntryAsState()를 통해 제공되며 화면 외부에서 컴포저블을 업데이트하기 위한 정보 소스로 사용됨.


  • NavHost 만들기
    각 NavController를 단일 NavHost 컴포저블과 연결해야 함
    NavHost는 구성 가능한 대상을 지정하는 NavGraph(=탐색그래프)와 NavController를 연결한다. 구성 가능한 대상은 Composable 간에 이동할 수 있어야 하는데, 여러 Composable 간에 이동하는 과정에서 NavHost의 컨텐츠가 자동으로 재구성됨.

NavHost를 만들려면 이전에 rememberNavController()를 통해 만든 NavController뿐만 아니라 Graph의 시작 대상 경로도 필요. NavHost를 만드는 데는 탐색 그래프를 생성하는 Navigation Kotlin DSL의 람다 구문이 사용된다. composable() 메서드를 사용하여 탐색 구조에 추가할 수 있음. 이 메서드를 사용하려면 경로뿐만 아니라 대상에 연결해야 할 컴포저블도 제공해야 된다.

@Composable
fun TestNavigation() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = TestScreens.SplashScreen.name) {
   		// composable() 메서드를 사용해 탐색 구조에 추가
        composable(TestScreens.SplashScreen.name) {
            TestSplashScreen(navController = navController) // Composable
        }
        composable(TestScreens.TestHomeScreen.name) {
            TestHomeScreen(navController = navController) // Composable
        }
    }

}

  • Composable로 이동
    탐색 그래프에서 구성 가능한 대상으로 이동하려면 navigate 메서드를 사용한다.
    navigate는 대상의 경로를 나타내는 단일 String 매개변수를 사용하고, 탐색 그래프 내의 컴포저블에서 이동하려면 navigate를 호출하면 된다.

(안드로이드 Navigation 컴포넌트에서 백 스택 관리를 위해 사용되는 NavController의 메서드인 navigate를 활용)

navController.navigate("friendslist")

기본적으로 navigate는 새로운 대상을 백 스택에 추가한다. 추가 탐색 옵션을 navigate() 호출에 연결하여 navigate 동작을 수정할 수도 있음.

  • popUpTo 메서드는 pop할 목적지를 지정할 수 있는 메서드로, 이 메서드를 사용하면 해당 목적지까지 모두 pop 할 수 있음.
// "home"이라는 목적지(destination)까지 백 스택에서 모두 pop하고, "friendslist"로 이동
navController.navigate("friendslist") {
    popUpTo("home")
}

// inclusive 속성이 true로 지정되어 있어 "home" 목적지도 포함해서 pop함.
navController.navigate("friendslist") {
    popUpTo("home") { inclusive = true }
}

// "search" 목적지로 이동할 때, 이미 "search" 목적지가 스택에 존재하는 경우 스택에서 pop하지 않고 기존의 목적지를 재사용. 이를 launchSingleTop 속성이 이 과정을 제어
navController.navigate("search") {
    launchSingleTop = true
}

  • 인수를 통해 이동
    Navigation Compose는 구성 가능한 대상 간의 인수 전달도 지원. 이렇게 하려면 기본 탐색 라이브러리를 사용할 때 딥 링크에 인수를 추가하는 방법과 유사한 방법으로 경로에 추가해야 함.
// startDestination으로 "profile/{userId}"를 설정했기 때문에 앱이 실행되면, 해당 URI가 바인딩된 화면으로 시작
NavHost(startDestination = "profile/{userId}") {
    // composable 함수 내부에 있는 "profile/{userId}"는 해당 URI가 호출되었을 때 보여질 목적지를 의미
    // 중괄호 안에 있는 {userId}는 동적으로 바뀔 수 있는 값을 의미하며, 해당 값은 라우팅 시에 파라미터로 전달됨. 예를 들어, "profile/123" 이라는 URI가 호출되면, {userId}에 123이라는 값이 전달되어 목적지를 렌더링하게 됨.
    composable("profile/{userId}") {...}
}

기본적으로 모든 인수는 문자열로 파싱됨. composable()의 arguments 매개변수는 NamedNavArgument 목록을 허용. navArgument 메서드를 사용하여 신속하게 NamedNavArgument를 만든 다음 정확한 type을 지정 가능.

NavHost(startDestination = "profile/{userId}") {
    // composable 함수의 두 번째 매개변수로 arguments를 전달하면 해당 목적지에 전달되는 파라미터들의 정보를 명시적으로 정의 가능
    //  "profile/{userId}" URI에 전달되는 {userId} 파라미터를 navArgument 함수를 통해 명시적으로 정의. 이때, type 속성을 통해 파라미터의 데이터 타입을 지정 가능. 이 코드에서는 NavType.StringType으로 지정하여 {userId} 파라미터의 데이터 타입을 문자열로 지정.
-> "profile/{userId}" 목적지에서 {userId} 파라미터를 사용할 때, 파라미터의 타입을 따로 체크하지 않고도 명시적으로 문자열 타입으로 사용 가능.
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

composable() 함수의 람다에서 사용할 수 있는 NavBackStackEntry에서 인수를 추출해서 사용하면 됨.

// composable 함수의 첫 번째 매개변수로는 해당 목적지의 URI를 전달
//"profile/{userId}"를 전달하여 해당 목적지에 도달하면 이 코드 블록이 실행되도록 설정
composable("profile/{userId}") { backStackEntry ->
// composable 함수의 두 번째 매개변수는 해당 목적지에 도달했을 때 표시될 컴포저블을 정의
//  이 예시에서는 Profile 컴포저블을 정의하고 navController와 backStackEntry.arguments?.getString("userId")를 매개변수로 전달하여 Profile 컴포저블을 실행
// backStackEntry.arguments?.getString("userId")는 userId 파라미터의 값을 가져옴. 이때, backStackEntry.arguments가 null일 가능성이 있으므로, ?.을 사용하여 null 체크를 하고, getString("userId")를 호출하여 실제 값이 존재할 때만 가져오도록 한다.
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

인수를 대상에 전달하려면 navigate 호출 시 경로에 추가해야 함.

navController.navigate("profile/user1234")

  • 선택적 인수 추가
    Navigation Compose는 선택적 탐색 인수도 지원함. 선택적 인수는 다음과 같은 두 가지 측면에서 필수 인수와 다르다.

쿼리 매개변수 문법("?argName={argName}")을 사용하여 포함해야 하는데,
defaultValue가 설정되어 있거나 nullability = true(암시적으로 기본값을 null로 설정함)가 있어야 한다.
즉, 모든 선택적 인수는 composable() 함수에 목록 형태로 명확하게 추가해야 함.

// composable 함수의 첫 번째 매개변수로는 해당 목적지의 URI를 전달
composable(
// "profile?userId={userId}"를 전달하여 해당 목적지에 도달하면 이 코드 블록이 실행되도록 설정
// {userId}는 쿼리 매개변수(query parameter)로, "profile" 목적지에 도달할 때 전달할 값
// composable 함수의 두 번째 매개변수는 해당 목적지에 도달했을 때 표시될 컴포저블을 정의.
// navArgument 함수를 사용하여 userId 매개변수를 정의하고, defaultValue를 "user1234"로 설정. defaultValue는 쿼리 매개변수가 전달되지 않았을 경우의 기본값을 설정하는데 사용
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
//backStackEntry 매개변수는 현재 NavBackStackEntry 객체를 전달하며, 해당 객체를 통해 arguments를 가져와서 Profile 컴포저블의 userId 파라미터를 설정. backStackEntry.arguments?.getString("userId")는 userId 파라미터의 값을 가져옴.
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

이제 대상에 전달되는 인수가 없더라도 'user1234' defaultValue가 대신 사용됨.

경로를 통해 인수를 처리하는 구조에서는 컴포저블이 Navigation과 완전히 독립적으로 유지되며 테스트 가능성이 훨씬 더 높다.


profile
https://github.com/nohjunh

0개의 댓글