[Compose Study] Navigation

728x90
반응형

* Navigation은 크게 3가지로 나뉜다. (NavController, NavGraph, NavHost)

  • NavController: 대상(즉, 앱의 화면) 간 이동을 담당한다. 
  • NavGraph: 이동할 컴포저블 대상을 매핑을 담당
  • NavHost: NavGraph의 현재 대상을 표시하는 컨테이너 역할을 하는 컴포저블

 

1. NavController

* NavController는 Navigation 구성요소의 중심 API로, 스테이트풀(Stateful)이며 앱의 화면과 각 화면 상태를 구성하는 컴포저블의 백 스택을 추적한다. 컴포즈 환경에서 NavController는 rememberNavController()를 이용하여 가져올 수 있다.

val navController = rememberNavController()

 

* rememberNavController()를 호출하여 NavContoller 인스턴스를 생성할 때 유의해야 할 점은 상태호이스팅에 유의해야 한다.

컴포저블 계층 구조에서 NavController를 만드는 위치는 이를 참조해야 하는 모든 컴포저블이 액세스할 수 있는 곳이어야 한다.

이러한 구조가 상태 호이스팅의 원칙을 준수하는 것이다.

 

2. NavGraph

NavGraph는 ID로 가져올 수 있는 NavDestination 노드의 집합이다. NavGraph는 '가상' 대상 역할을 한다.

NavGraph 자체는 백 스택에 나타나지 않지만 NavGraph로 이동하면 시작 대상이 백 스택에 추가된다.

새로운 NavGraph는 대상을 추가하고 시작 경로을 설정할 때까지 유효하지 않다.

대상을 추가하는 방법은 NavGraphBuilder.composable() 함수를 이용한다. 자세한 내용은 아래에서 설명한다.

 

3. NavHost

* NavHost 는 코드에 의해서 구현되는 Navigation Graph와 NavController 를 연결시켜줍니다.

이렇게 하면, 특정한 destination (route에 의한 도착지점) 의 Composable로 갈 수 있게 된다.

코드에 의해서 구현되는 Navigation Graph는 route에 따라서, 각 Screen들로 연결됩니다.

이렇게 route에 의해서 도착되는 지점들을 destination이라고 부른다.

public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
)

 

NavHost의 원형을 살펴보면 위와 같다.

  • navController: NavHostController 클래스의 인스턴스이다. navigate() 메서드를 호출하여 다른 대상으로 이동하는 등의 방식으로 화면 간에 이동하는 데 이 객체를 사용할 수 있다.
  • startDestination: 앱에서 NavHost를 처음 표시할 때 기본적으로 표시되는 대상을 정의하는 문자열 경로이다.
  • builder: NavGraphBuilder로 그래프를 생성하기 위해 사용되는 빌더이다. 여기에 NavGraphBuilder의 composable() 함수를 이용하여 경로 대상을 정의 할 수 있다.

 

* 참고로 여기서 말하는 route라는 것은, Navigation Graph를 형성하는 composable로 향하는 path를 말한다.

당연하게도 각각의 Screen, Navigation 상의 Destination들은 중복되지 않는 유니크한 값을 필요로 한다.

composable함수의 인자에 route를 넣게되면, 아래 그림과 같은 Navigation Graph 가 형성되게 되는 것이다.

 

 

* composable("라우트") 함수들을 이용해서 Navigation Graph를 생성할 때, 지켜야 하는 규칙들이 있는데, 그 중 하나가 바로 Fixed Start Destination 이다.

당연할 수도 있는 것이지만, 시작점과 다시 돌아올 수 있는 끝점이 있어야 한다는 것 이다.

아래는 안드로이드 공식문서에 나온 예시이다. "List Screen"이 바로 Fixed Start Destination이다.

 

* 참고로 로그인화면이나 유저이름 설정화면과 같이,

처음에만 조건부로 들어가는 화면들은 이러한 고정된 시작점(Fixed Start Destination) 이 되면 안된다.

 

* Destination 간 이동하기

* NavHost 안에서 composable("라우트")들을 구성함으로서, Destination Graph를 생성할 수 있고,

유니크한 라우트 값에 따라서 Destination 으로 이동할 수 있다.

navigate()함수는 특정 Destination 으로 이동하도록 해준다.

아래와 같이 하면, setting 이라고 하는 라우트로 이동하도록 해준다.

navController.navigate("setting")
Button(onClick = {
    navController.navigate("setting")
}) {
    Text(text = "setting 으로 이동하기")
}

* 버튼으로 만들수도 있다.

 

* 사용 예시

의존성 추가 (build.gradle)

implementation("androidx.navigation:navigation-compose:2.8.0")

 

경로 추가

enum class TestScreen() {
    First,
    Second,
    Third
}

 

NavController 추가

@Composable
fun TestApp(modifier: Modifier = Modifier){

    val navController = rememberNavController()
    ...
}

 

NavHost 추가 및 경로 정의

 

* NavHost를 추가하고 내부에 NavGraphBuilder.composable() 함수를 이용하여 경로를 정의한다. composable은 2개의 매개변수가 필요하다.

  • route: 경로 이름에 해당하는 문자열입니다. 모든 고유 문자열을 사용할 수 있습니다. CupcakeScreen enum의 상수 이름 속성을 사용합니다.
  • content: 여기에서 특정 경로에 표시할 컴포저블을 호출할 수 있습니다.

* route는 위에서 정의한 경로 (enum class를 사용했었다)를 이용하여 route 매개변수를 전달하고, content는 컴포저블 함수로 Screen 등 표시할 컨텐츠가 포함된다.

@Composable
fun TestApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()
    ...

    NavHost(
        navController = navController,
        startDestination = TestScreen.First.name,
        modifier = modifier.padding(innerPadding)
    ) {
        composable(route = TestScreen.First.name) {
            FirstScreen(
                ...
            )
        }
        composable(route = TestScreen.Second.name) {
            SecondScreen(
                ...
            )
        }
        composable(route = TestScreen.Third.name) {
            ThirdScreen(
                ...
            )
        }
    }
}

 

* NavHost 의 매개변수를 살펴보면, 위에서 생성한 navController를 전달한다. 그리고 처음 시작점인 startDestination으로 TestScreen.First.name을 이용해 첫 경로를 설정하여 준다.

경로 간 이동하기

* 경로 간 이동을 위해 navController의 navigate() 함수를 이용한다. 아래 코드를 보면, FirstScreen의 버튼 클릭 이벤트에서 SecondScreen으로 이동하는 코드이다.

@Composable
fun TestApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()
    ...

    NavHost(
        navController = navController,
        startDestination = TestScreen.First.name,
        modifier = modifier.padding(innerPadding)
    ) {
        composable(route = TestScreen.First.name) {
            FirstScreen(
                ...,
                onNextButtonClicked = {
                    navController.navigate(TestScreen.Second.name)
                }
            )
        }
        composable(route = TestScreen.Second.name) {
            SecondScreen(
                ...
            )
        }
    }
}

 

* Route 이동 시 값 전달

 1. route, argument로 전달

* composable 에 다음의 2가지 인자를 넣어준다.

  • route
    • setting/{userId} : userId라는 dynamic한 값 전달
      • 예를 들어, setting/123 으로 route를 넘기면, 123이라는 값을 전달할 수 있게 됩니다.
  • arguments
    • 라우트를 통해 전달받은 값들의 list 
    • navArgument("userId"){ type = NavType.IntType }
      • 여기서 argument 는 userId로 넘기는 값의 이름입니다.
      • type은 넘기는 데이터의 타입을 지정해주는 것 입니다. 여기서는 Int 타입입니다.
@Composable
fun MyNav() {
    val navController = rememberNavController()
    NavHost(navController, startDestination = "setting/123") {
        composable(
        	"setting/{userId}", 
        	arguments = listOf(navArgument("userId") { 
        		type = NavType.IntType 
            })) { backStackEntry ->
                val userId = backStackEntry.arguments?.getInt("userId")
                SettingScreen(userId = userId)
        }
        composable("home") {
            Button(onClick = { navController.navigate("setting/456") }) {
                Text("Go to Setting")
            }
        }        
    }
}

@Composable
fun SettingScreen(userId: Int?) {
    // Some UI code for setting screen...
    Text("Setting for User: $userId")
}

 

* 만약 타입에 null이 사용이 가능하다면, nullable은 true 로 해주고 defaultValue를 null로 해 줄수도 있다.

composable(
    "setting/{name}", 
    arguments = listOf(
        navArgument("name") { 
            type = NavType.StringType
            nullable = true
            defaultValue = null
        }
    )
) { backStackEntry ->
    val name = backStackEntry.arguments?.getString("name")
    SettingScreen(name = name)
}

 

* Route에 데이터를 넘길때 String 으로부터 파싱해서 넘겨지기 때문에, 복잡한 형태의 Custom Data를 넘기지 못한다.

당연히 Moshi나 Gson같은 것으로 Serialize 해서 넘길수도 있겠지만, 그러한 것은 권장되지 않음.

기본적으로는 복잡한 형태를 Destination간에 넘기는 것을 지양하도록 되어 있다.

 

2. savedStateHandle 을 이용해서 viewModel 에서 받아오기

* 위에서 보았던 것과 같이, 값을 해당 Screen 을 호출할 때 넘겨줄수도 있지만, 아래와 같이 viewModel에서 받아올 수도 있다.

@HiltViewModel
class SettingViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    val userId: Int? = savedStateHandle.get<Int>("userId")
}

 

* https://developer88.tistory.com/entry/Route-에서-넘어온-값을-ViewModel-savedStateHandle-로-받는-방법-Jetpack-Compose-Navigaion

 

Route 에서 전달된 값을 ViewModel 에서 받는 방법 # Jetpack Compose Navigaion savedStateHandle

오늘은 Route에서 넘어온 값을 ViewModel 에서 savedStateHandle 을 이용해 받아서 사용하는 방법에 대해서 정리해 보겠습니다. 1. Route에서 값을 넘겨줄 때 먼저 route에서 값을 넘겨주는 코드를 보고 가겠

developer88.tistory.com

 

 

 

------

 

* 참고자료: https://developer88.tistory.com/entry/Navigation-총정리-nested-Jetpack-Compose

 

Navigation 구현 방법 총정리 # Route Jetpack Compose

오늘은 Jetpack Compose 에서 구현하는 Navigation 에 대해서 정리해 보도록 하겠습니다. 1. Navigation Library 가장 먼저 준비할 것은 navigation 구현을 위해 라이브러리를 implement 하는 것 입니다. 아래의 라이

developer88.tistory.com

 

728x90
반응형
TAGS.

Comments