#1 개요
#1-1 전통적인 안드로이드 프로젝트에서의 Navigation
위 게시글은 Jetpack Compose가 쓰이지 않은 전통적인 구조의 안드로이드 프로젝트에서의 Navigation에 대해 다룬 게시글이다.
#1-2 Jetpack Compose 구조의 안드로이드 프로젝트에서의 Navigation
본 게시글은 Jetpack Compose 구조에서 사용 가능한 Navigation에 대해 다뤄본다. #1-1과 하는 동작은 동일하지만, 구현 방법은 꽤 다르다.
#2 기제
#2-1 개요
NavHost 객체가 화면 전환을 주관한다. 화면 전환을 트리거(발생)시키는 객체는 NavHostController로, 마치 TV 리모콘과 같은 기능을 한다고 보면 된다. NavHost에 의해 관리될 각각의 화면 컴포저블을 Destination(목적지)이라 하며, 그 Destination은 route라는 String형 인수를 가진다. 이 route는 각 Destination을 식별(구분)하는데 사용된다. 따라서, 같은 NavHost 내에서 route의 이름은 중복됨 없이 고유(unique)해야 한다.
#2-2 도식도
자유 형식으로 그린 도식도다. #3에서 실제 코드로 구현한다.
#2-3 NavHost
@Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
route: String? = null,
enterTransition: @JvmSuppressWildcards() (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) },
exitTransition: @JvmSuppressWildcards() (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) },
popEnterTransition: @JvmSuppressWildcards() (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = enterTransition,
popExitTransition: @JvmSuppressWildcards() (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = exitTransition,
sizeTransform: @JvmSuppressWildcards() (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null,
builder: NavGraphBuilder.() -> Unit
): Unit
Navigation 동작을 주관하는 객체는 NavHost라는 @Composable 함수다. 이전 게시글(#1-1)에서는 Activity 또는 Fragment의 전환을 위해 Navigation graph라는 일종의 지도를 사용했었다. Jetpack Compose의 Navigation에서 Navigation graph의 역할을 수행하는 것이 바로 NavHost다.
각 인수에 대한 설명은 아래와 같다.
navController: NavHostController
NavHost가 지도라면, NavHostController는 순간이동 장치다. 지도 위에서 가고싶은 목적지(route)를 선택하여 이동할 수 있다. 화면 간의 전환을 트리거(발생)시키기 위해서는 NavHostController를 사용한다. TV 리모콘하고 비슷하다.
startDestination: String
내비게이션 그래프에서 기본적으로 표시될 하위 컴포저블이다. NavHost는 Composable 함수지만, 눈에 보이지 않는다. 자기 자신 대신 startDestination으로 지정된 하위 컴포저블을 표시한다.
route: String?
NavHost의 식별자. 하나의 NavHost가 가지고 있는 하위 컴포저블인 목적지(Destination)의 식별자(route)하고는 다른 개념이다. 앱 내에서 NavHost가 여러 개 사용되는 경우에 사용되는 인수다. 기본값은 null로 지정되어있다.
@JvmSuppressWildcards()
자바와의 호환성을 위해 사용되는 어노테이션. 이 어노테이션을 생략하면 NavHost를 자바 환경에서 사용할 때, 인수 데이터형에 "? super" 및 "? extends" 등의 와일드카드("?") 구문이 추가되어 코드가 더러워진다고 한다.
enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
다른 Destination이 종료되며 현재 Destination로 전환했을 때 나오는 애니메이션.
(참조: 전통적인 구조에서의 Navigation 애니메이션의 #2-2)
exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
현재 Destination이 종료될 때 다른 Destination으로 전환되며 나오는 애니메이션.
(참조: 전통적인 구조에서의 Navigation 애니메이션의 #2-3)
popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
(사용자의 뒤로가기 버튼 클릭으로 인해) 이전 Destination이 종료되며 현재 Destination로 전환했을 때 나오는 애니메이션.
(참조: 전통적인 구조에서의 Navigation 애니메이션의 #2-4)
popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
(사용자의 뒤로가기 버튼 클릭으로 인해) 현재 Destination이 종료될 때 다른 Destination으로 전환되며 나오는 애니메이션.
(참조: 전통적인 구조에서의 Navigation 애니메이션의 #2-5)
sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)?
화면 전환 시 크기가 변경되는 애니메이션을 추가한다. 기본값은 null이다.
builder: NavGraphBuilder.() -> Unit
NavHost가 가질 Destination들을 정의.
#2-4 builder: NavGraphBuilder.() -> Unit
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
): Unit
NavHost에 정의된 마지막 인수이며, NavHost가 보유하는 Destination의 정체이기도 하다.
각 인수에 대한 설명은 아래와 같다.
route: String
Destination끼리 식별하기 위한 이름.
arguments: List<NamedNavArgument>
Destination 간 전달할 데이터의 리스트. 화면 전환 시의 데이터 전달이 목적이다. 마치 웹 프로그래밍에서 URL에 데이터를 실어보내는 것(GET 요청)과 비슷하게, route에 이 arguments를 넣어 실어보낸다. 이어지는 글(#8)에서 자세히 다룬다.
deepLinks: List<NavDeepLink>
해당 Destination에 진입할 수 있는 딥 링크들의 리스트. 딥 링크는 앱 외부(웹 브라우저, QR 코드, 타 앱)에서 특정 Destination으로 바로 접근할 수 있는 링크를 말한다고 한다. 본 게시글에선 다루지 않는다.
content: @Composable (NavBackStackEntry) -> Unit
Destination에 해당하는 컴포저블 함수. NavBackStackEntry는 Destination의 정보를 담고 있는 객체로, arguments에 저장한 데이터에 접근하기 위한 통로로 사용된다 (이어지는 글(#8) 참조).
#3 구현
#3-1 모듈 수준 build.gradle.kt
plugins {
...
}
android {
...
}
dependencies {
...
// Navigation
implementation("androidx.navigation:navigation-runtime-ktx:2.8.0")
implementation("androidx.navigation:navigation-compose:2.8.0")
}
#3-2 NavHost
// package com.example.navigationbasics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun MyNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController()
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = "firstScreen"
) {
// (1) FirstScreen
composable(route = "firstScreen") {
FirstScreen(navController)
}
// (2) SecondScreen
composable(route = "secondScreen") {
SecondScreen(navController)
}
}
}
MyNavHost는 Activity의 setContent { ... }에 넣을 컴포저블이다 (#3-5 참조). 위 코드에서 MyNavHost 및 NavHost의 modifier를 하위 컴포저블인 FirstScreen 및 SecondScreen에 전달하고 있지 않으므로, 그 존재가 무의미하다. 삭제하는 편이 더 깔끔한 코드겠지만, Activity로부터 Modifier를 이어받아 NavHost를 거쳐 Destination까지 전달이 가능하다는 걸 보이고 싶기에 그냥 냅두겠다.
#3-3 첫번째 Destination (FirstScreen)
// package com.example.navigationbasics
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
@Composable
fun FirstScreen(navController: NavHostController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "First Screen")
Button(
onClick = { navController.navigate("secondScreen") }
) {
Text(text = "Go to Second Screen")
}
}
}
NavHostController가 어떻게 동작하는지 확인할 수 있다.
#3-4 두번째 Destination (SecondScreen)
// package com.example.navigationbasics
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
@Composable
fun SecondScreen(navController: NavHostController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "Second Screen")
Button(
onClick = { navController.navigate("firstScreen") }
) {
Text(text = "Go to First Screen")
}
}
}
#3-3과 같은 맥락의 코드다.
#3-5 Activity
// package com.example.navigationbasics
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import com.example.navigationbasics.ui.theme.NavigationBasicsTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NavigationBasicsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
MyNavHost(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
MyNavHost는 컴포저블이지만 형태가 없다. 대신, NavHost()의 인수 중 하나인 startDestination에 적힌 Destination을 대신 보여준다.
#4 작동 확인
#5 코드 리팩토링: 단일 진실 공급원(single source of truth, SSOT) 원칙
#5-1 SSOT 원칙?
위 원칙을 한 마디로 정의하면, "데이터 조작 동작을 한 곳으로 몽땅 모음"이다. 이 원칙에 기반하여 #3의 코드를 리팩토링해본다.
#5-2 NavHost
// package com.example.navigationbasics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun MyNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController()
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = "firstScreen"
) {
// (1) FirstScreen
composable(route = "firstScreen") {
FirstScreen(navigateToSecondScreen = { navController.navigate("secondScreen") })
}
// (2) SecondScreen
composable(route = "secondScreen") {
SecondScreen(navigateToFirstScreen = { navController.navigate("firstScreen") })
}
}
}
FirstScreen 및 SecondScreen에 NavHostController를 주는 대신, NavHostController의 한 동작(이벤트)를 주게끔 바꾼다. 즉, FirstScreen에서 조작할 NavHostController의 동작을 미리 NavHost단에서 정의하여 내려준다. 이를 SSOT 패턴이라고 하며, 이 패턴을 구현함으로써 (기존 대비 변수를 보내주지 않는다는 점에서) 객체 간 결합도가 낮아졌다.
#5-3 FirstScreen
// package com.example.navigationbasics
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun FirstScreen(navigateToSecondScreen: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "First Screen")
Button(
onClick = { navigateToSecondScreen() }
) {
Text(text = "Go to Second Screen")
}
}
}
Button의 onClick 부분을 수정한다. SecondScreen도 이와 같은 맥락으로 고칠 수 있으므로 생략한다.
#6 요약
NavHost는 TV 채널 목록, NavController는 TV 리모콘이다.
#7 완성된 앱
#8 이어지는 글
#8-1 Destination 간 데이터 전달 (NavBackStackEntry.arguments)
#8-2 NavigationBar
[게시글 작성 및 링크 추가 예정]
'깨알 개념 > Android' 카테고리의 다른 글
[Android] Jetpack Compose - Navigation의 Destination 간 데이터 전달 (NavBackStackEntry.arguments) (0) | 2024.09.13 |
---|---|
[Android] Jetpack Compose - 객체 지향적 UI 레이어 설계 (0) | 2024.09.11 |
[Android] Jetpack Compose - Side-effects (0) | 2024.09.06 |
[Android] Jetpack Compose - Scaffold (0) | 2024.09.05 |
[Android] LiveData - Flow로 마이그레이션 (0) | 2024.08.29 |