-
Android Compose Navigation(Jetsnack sample 분석)개발/Android 2024. 3. 23. 18:11
Navigation
내비게이션은 간단하게 설명하면 화면 전환을 도와주는 Jetpack 라이브러리다. Compose를 사용하기 전에도 내비게이션은 자주 사용해봤다. 나는 Activity안에 여러개의 Fragment를 내비게이션으로 이동하는 방식으로 자주 사용했다. 기존에는 xml폴더에 그래프를 만들어 연결하는 작업이 있었지만 Compose는 코드로 다 해결한다.
간단하게 개념을 정리하고 Android 공식 샘플인 Jetsnack에서의 코드를 분석해보려고 한다.
https://github.com/android/compose-samples
구성요소
내비게이션은 크게 3가지 구성요소가 있다.
- NavController: 화면 간의 이동 담당
- NavGraph: 이동할 Composable 매핑 담당
- NavHost: NavGraph의 현재 대상 표시하는 컨테이너 역할
NavController
- StateFull(상태를 가지는 상태)
- 앱의 화면, 각 화면 상태를 구성하는 Composable 백스택 추척
- remberNavController()로 가져올 수 있다.
val navController = rememberNavController()
Composable 계층 구조에서 NavController를 만드는 위치는 모든 Composable이 참조할 수 있는 곳이여야 한다(상태호이스팅)
간단히 어디서든 접근할 수 있게 말해 계층 구조에서 가장 상위 레벨에 선언한다는 뜻이다.NavHost
- NavController와 NavGraph를 연결
public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, route: String? = null, builder: NavGraphBuilder.() -> Unit )
보통 이런식으로 선언한다. 인자로 NavController와 NavGraphBuilder 받는다.
NavGraphBuilder의 확장함수인 composable 함수를 이용할 수 있다.
@Composable fun MyApp() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "screen1") { composable("screen1") { Screen1(navController) } composable("screen2") { Screen2(navController) } } } @Composable fun Screen1(navController: NavController) { Column { Text(text = "Screen 1", color = Color.Black) Button(onClick = { navController.navigate("screen2") }) { Text(text = "Navigate to Screen 2") } } } @Composable fun Screen2(navController: NavController) { Column { Text(text = "Screen 2", color = Color.Black) Button(onClick = { navController.popBackStack() }) { Text(text = "Go back to Screen 1") } } }
NavGraph
- 이동할 Composable 매핑 담당
- 화면간의 관계를 나타낸다.
- 보통은 위의 예처럼 NavGraphBuilder의 함수를 써서 정의한다.
Jetsnack 코드 분석
Jetsnack 앱은 중첩된 Navigation을 사용한다. 아래 그림을 참고하면 감이 올 것이다.
2개의 내비게이션을 사용하여 뎁스가 2개라고 이해하면 된다. 상단 뎁스에는 Home, SnackDetail 화면이 있다. 왼쪽이 Home, 오른쪽이 SnackDetail이다. SnackDetail은 중첩 구조가 아닌 단일 composable함수이다. 반면에 Home 아래 4개의 메뉴를 가진 BottomBar가 있다.
정리하면 이 앱의 내비게이션 구조는 {Home{Feed, Search, Cart, Profile}, SnakDetatil} 이렇게 나타낼 수 있다.
코드 최상단
@Composable fun JetsnackApp() { JetsnackTheme { val jetsnackNavController = rememberJetsnackNavController() NavHost( navController = jetsnackNavController.navController, startDestination = MainDestinations.HOME_ROUTE ) { jetsnackNavGraph( onSnackSelected = jetsnackNavController::navigateToSnackDetail, upPress = jetsnackNavController::upPress, onNavigateToRoute = jetsnackNavController::navigateToBottomBarRoute ) } } }
- JetsnackNavController 라는 커스텀 NavController를 만들어서 사용한다. 최상위에 controller 선언(상태 호이스팅)
- NavHost에 controller와 startDestination을 설정한다.
@Composable public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, route: String? = null, enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) }, exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) }, popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = enterTransition, popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = exitTransition, builder: NavGraphBuilder.() -> Unit ) { NavHost( navController, remember(route, startDestination, builder) { navController.createGraph(startDestination, route, builder) }, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition ) }
- NavHost 함수의 원형이다. builder 인자로 NavGraphBuilder의 확장함수를 받는다
private fun NavGraphBuilder.jetsnackNavGraph( onSnackSelected: (Long, NavBackStackEntry) -> Unit, upPress: () -> Unit, onNavigateToRoute: (String) -> Unit ) { navigation( route = MainDestinations.HOME_ROUTE, startDestination = HomeSections.FEED.route ) { addHomeGraph(onSnackSelected, onNavigateToRoute) } composable( "${MainDestinations.SNACK_DETAIL_ROUTE}/{${MainDestinations.SNACK_ID_KEY}}", arguments = listOf(navArgument(MainDestinations.SNACK_ID_KEY) { type = NavType.LongType }) ) { backStackEntry -> val arguments = requireNotNull(backStackEntry.arguments) val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) SnackDetail(snackId, upPress) } }
- NavGraph에 연결한 내비게이션 정보를 세팅한다.
- 여기서 route는 내비게이션의 고유한 경로를 나타낸다.
- navigation 함수로 하위 내비게이션을 정의한다.
public inline fun NavGraphBuilder.navigation( startDestination: String, route: String, builder: NavGraphBuilder.() -> Unit ): Unit = destination(NavGraphBuilder(provider, startDestination, route).apply(builder)) // destination의 원형 public fun <D : NavDestination> destination(navDestination: NavDestinationBuilder<D>) { destinations += navDestination.build() }
- navigation 함수의 원형이다. builder 인자로 NavGraphBuilder 확장함수를 받는다.
- destination은 내비게이션 그래프에 새로운 목적지를 추가하는 역할이다.
- NavDestination 객체를 생성 후 그래프의 destination에 추가한다.
Home섹션의 하위 내비게이션을 정의한다(4개의 화면으로 나눠지는 화면).
fun NavGraphBuilder.addHomeGraph( onSnackSelected: (Long, NavBackStackEntry) -> Unit, onNavigateToRoute: (String) -> Unit, modifier: Modifier = Modifier ) { composable(HomeSections.FEED.route) { from -> Feed(onSnackClick = { id -> onSnackSelected(id, from) }, onNavigateToRoute, modifier) } composable(HomeSections.SEARCH.route) { from -> Search(onSnackClick = { id -> onSnackSelected(id, from) }, onNavigateToRoute, modifier) } composable(HomeSections.CART.route) { from -> Cart(onSnackClick = { id -> onSnackSelected(id, from) }, onNavigateToRoute, modifier) } composable(HomeSections.PROFILE.route) { Profile(onNavigateToRoute, modifier) } }
- Home 섹션의 NavGraph는 다시 4개의 composable 함수를 통해 정의된다.
- 매개변수로 받은 함수를 넘겨서 클릭 시 이벤트를 정의한다.
composable( "${MainDestinations.SNACK_DETAIL_ROUTE}/{${MainDestinations.SNACK_ID_KEY}}", arguments = listOf(navArgument(MainDestinations.SNACK_ID_KEY) { type = NavType.LongType }) ) { backStackEntry -> val arguments = requireNotNull(backStackEntry.arguments) val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) SnackDetail(snackId, upPress) } //함수 원형 public fun NavGraphBuilder.composable( route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = null, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = null, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = exitTransition, content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit ) { addDestination( ComposeNavigator.Destination( provider[ComposeNavigator::class], content ).apply { this.route = route arguments.forEach { (argumentName, argument) -> addArgument(argumentName, argument) } deepLinks.forEach { deepLink -> addDeepLink(deepLink) } this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition } ) }
- SnackDetail 화면을 정의하는 composable 함수이다.
- content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit 은 내비게이션 목적지로 도착했을 때 실행 될 compose UI를 정의하는 부분이다.
- NavBackStackEntry 는 화면이 이동하면서 쌓이는 각 목적지에 대한 정보를 나타내는 객체이다.(현재 화면에 대한 정보를 나타낸다)
- 인자 전달(arguments 이용), 라이프사이클 관리(LifecycleOwner 인터페이스 구현) 등 정보를 가지고 있다.
"${MainDestinations.SNACK_DETAIL_ROUTE}/{${MainDestinations.SNACK_ID_KEY}}", arguments = listOf(navArgument(MainDestinations.SNACK_ID_KEY) { type = NavType.LongType })
이 부분이 가장 헷갈렸다. 화면으로 이동할 때 특정 데이터를 전달해 그 데이터에 맞는 화면을 보여주는 코드이다(여기서는 Snack ID 값을 던져줘 해당 스낵의 화면을 보여주는 예시).
- "${MainDestinations.SNACK_DETAIL_ROUTE}/{${MainDestinations.SNACK_ID_KEY}}" 이 코드가 route를 나타내는 코드이다. {} 넣는 값(여기서는 {${MainDestinations.SNACK_ID_KEY}} )은 던져줄 매개 변수가 들어가는 자리라고 이해하면 된다.
- arguments = listOf(navArgument(MainDestinations.SNACK_ID_KEY) { type = NavType.LongType }) 이 코드에서 MainDestinations.SNACK_ID_KEY 가 다시 나온다. argument를 선언하고 type을 지정하면 route에 해당 타입의 데이터를 붙여줘서 화면을 부르는 방식이다.
- 예를 들어 여기서는 LongType 이므로 navigate 호출시 Long id 값을 붙여서 전달 하는 것이다.
fun navigateToSnackDetail(snackId: Long, from: NavBackStackEntry) { if (from.lifecycleIsResumed()) { navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId") } }
실제 내비게이션 이동 시 호출하는 함수이다. Long 타입의 id를 route에 붙여서 넘겨준다.
val arguments = requireNotNull(backStackEntry.arguments) val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) SnackDetail(snackId, upPress)
key 값을 이용해 arguments에서 데이터를 뽑아서 사용할 수 있다.
'개발 > Android' 카테고리의 다른 글
Compose 상태 (0) 2024.04.06 Compose 개요 (0) 2024.03.09