내비게이션은 Compose의 화면 전환 라이브러리이다. 쓰레기인 xml과 다르게 화면 전환이 훨씬 쉽다고 한다. 지금부터 공부해 보자
우선 다음과 같은 의존성을 앱 수준 gradle에 추가해 준다
implementation("androidx.navigation:navigation-compose:2.7.7")
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = )
}
그 후 Navigation function을 정의한다.
NavController는 화면 이동을 “지휘하는 컨트롤 타워” 역할을 하는 객체로, 다음과 같은 특징이 있다.
- 지금 어떤 화면(route)에 있는지 기억하고
- 다음 화면으로 이동시키고
- 뒤로가기 스택(back stack)을 관리한다.
NavHost는 "어떤 route에 어떤 화면(Composable)을 보여줄지 연결해주는 지도" 역할을 한다. NavController가 이동 담당이라면, NavHost는 "화면 등록 + 연결"의 역할을 하면 본다고 된다.
startDestination을 정의하기 전에, 별도의 클래스를 하나 만들어 준다.
sealed class Screen(val route: String) {
//object : 싱글톤 객체
object MainScreen : Screen("main_screen")
object DetailScreen : Screen("detail_screen")
}
route는 NavController가 화면을 구분하기 위해 쓰는 각 화면마다의 고유한 문자열(String) 키 이다. 이 문자열로
- 어떤 화면으로 이동할지
- 현재 어떤 화면에 있는지
를 판단한다.
sealed class는 상속 가능한 클래스인데, 상속할 수 있는 애들을 ‘내가 정한 애들만' 허용하는 클래스이다. sealed class를 Compose Navigation에서 쓰는 이유는 화면 목록을 안전하게 관리하기 위함인데, 앱에 존재하는 화면 목록을 타입으로 고정하기 위해 사용한다. 화면 개수를 컴파일 타임에 고정하고, 또한 각 화면을 싱글톤 객체로 관리함으로써 route 문자열의 오타를 방지하고, 화면 간 이동을 Type-safe하게 처리할 수 있다.
Navigation function을 마저 작성해 보자
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.MainScreen.route) {
//composable : 이 route로 들어오면, 이 화면을 그려라
composable(route = Screen.MainScreen.route) {
}
}
}
composable 블록은 "route가 MainScreen.route일 때, 이 중괄호 안에 있는 Composable을 화면으로 보여줘라" 라고 해석하면 된다.
이제 예시로 쓸 두 화면인 MainScreen과 DetailScreen 컴포저블을 정의해 주자.
@Composable
fun MainScreen(navController: NavController) {
var text by remember {
mutableStateOf("")
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 50.dp)
) {
TextField(value = text, onValueChange = { //(String) -> Unit
text = it //newText -> text = newText
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
//중요!!!!!
Button(onClick = {
navController.navigate(Screen.DetailScreen.route)
},
modifier = Modifier.align(Alignment.End)) {
Text(text = "To DetailScreen")
}
}
}
@Composable
fun DetailScreen(name: String?) {
Box(contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello $name")
}
}
이런 식으로 간단하게 만들었다. 저기서 Button의 onClick 부분에 주목해 보자. onClick은 버튼이 눌렸을 때 실행되는 람다 함수이며, 이 안에서 NavController의 navigate()를 호출해 다른 화면으로 이동한다.
navigate()가 호출되면, 현재 화면은 back stack에 쌓이고, 목적지 화면이 새로 그려진다.
하지만 아직 composable메서드를 다 정의하지 않았다. 마저 해보자.
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.MainScreen.route) {
//composable : 이 route로 들어오면, 이 화면을 그려라
composable(route = Screen.MainScreen.route) {
MainScreen(navController = navController)
}
composable(
route = Screen.DetailScreen.route,
arguments = listOf(
navArgument("name") {
type = NavType.StringType //전달받을 값의 자료형
defaultValue = "Phillip" //디폴트 전달값
nullable = true //nullable 허용
}
)
) { entry ->
DetailScreen(name = entry.arguments?.getString("name")) //번들로 전달
}
}
}
갑자기 많은 것이 튀어나왔다. 하지만 어려울 건 없다. xml로 ui를 구현할 때 액티비티<->액티비티 또는 프래그먼트<->프래그먼트 간의 데이터 전달을 Bundle로 했던 것을 기억하는가? Bundle로 전달하는 것은 똑같지만, 여기서는 더 쉽다. navArgument는 “이 화면이 받을 수 있는 데이터(인자)의 명세서”이다. 위의 방식으로 넘겨줄 인자의 자료형, 값이 안 넘어왔을 때 자동으로 넣어줄 값, null허용 여부 등을 정의할 수 있다.
그런데 사실 route는 단순한 문자열이 아니다. Compose Navigation에서 route는 그냥 문자열이 아니라 URL 패턴이라고 생각해야 한다. 인자를 효과적으로 전달하기 위해선 "detail_screen"과 같은 고정 route가 아닌, "detail_screen/{name}"과 같은 동적 route를 쓸 필요가 있다. 이를 구현하여 보자.
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.MainScreen.route) {
//composable : 이 route로 들어오면, 이 화면을 그려라
composable(route = Screen.MainScreen.route) {
MainScreen(navController = navController)
}
composable(
route = Screen.DetailScreen.route + "/{name}", //인자를 추가할때마다 /{...} 을 추가.
arguments = listOf(
navArgument("name") {
type = NavType.StringType //전달받을 값의 자료형
defaultValue = "Phillip" //디폴트 전달값
nullable = true //nullable 허용
}
)
) { entry ->
DetailScreen(name = entry.arguments?.getString("name")) //번들로 전달
}
}
}
sealed class Screen(val route: String) {
//object : 싱글톤 객체
object MainScreen : Screen("main_screen")
object DetailScreen : Screen("detail_screen")
//route 문자열을 안전하고 깔끔하게 만들어주는 헬퍼 함수
//vararg : 가변 개수 인자
//buildString : 문자열을 효율적으로 생성하기 위한 Kotlin 표준 함수
fun withArgs(vararg args: String) : String {
return buildString { //
append(route)
args.forEach { args ->
append("/$args")
}
}
}
}
우선 Screen 클래스에 route 뒤에 인자들을 /값 형태로 자동으로 붙여주는 헬퍼 함수를 추가한다. 그 후,
Button(onClick = {
navController.navigate(Screen.DetailScreen.withArgs(text))
}
이런 식으로 클릭 이벤트를 수정해 준다.
마지막으로 메인액티비티에서 Navigation function을 호출해 준다.
class MainActivity : ComponentActivity() {
@SuppressLint("InvalidColorHexValue")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
//xml은 setContentView
setContent {
Navigation()
}
}
}
완성!!!!
'안드로이드 스터디' 카테고리의 다른 글
| JetPack Compose 9편 : ConstraintLayout (0) | 2025.12.31 |
|---|---|
| JetPack Compose 7편 : Lists (0) | 2025.12.31 |
| JetPack Compose 6편 : State (0) | 2025.12.31 |
| JetPack Compose 5편 : 텍스트 스타일링 (0) | 2025.12.30 |
| JetPack Compose 4편 : 이미지 카드 만들기 (1) | 2025.12.30 |