[Android] Jetpack Navigation — Migrate to Compose — Graph & Destination

Kenneths
Kenneth Android
Published in
18 min readDec 22, 2022
Photo by Jean-Frederic Fortier on Unsplash

이번 글에서는 Navigation에 필요한 그래프를 만들어보고 Destination을 설정하여 대상간 이동하는 방법까지 다룹니다. Compose Navigation 만 궁금하신 분은 Fragment Navigation을 건너뛰고 Compose Navigation부터 참고해주세요. 😄

Fragment Navigation

의존성추가

def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

NavGraph 만들기

res -> navigation 폴더 생성 후 app_navigation.xml 파일 생성

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/appNavigation"
app:startDestination="@id/mainFragment">

<fragment
android:id="@+id/mainFragment"
android:name="com.kennethss.android.navigation.ui.MainFragment"
android:label="@string/label_main"
tools:layout="@layout/fragment_main">
</fragment>
</navigation>

<navgation> 안의 요소들은 Graph의 Root 요소들이 들어가 있습니다. id , startDestination 이 해당 요소에 포함됩니다.

Navigation Graph의 하위 요소들은 destination action 들로 구성되고 중첩 그래프(Nested Graph)를 작성하고 싶을 때에는 navigation 요소로서 사용할 수 있습니다.

Navigation Graph

Navigation Graph를 설정을 완료하면 Editor에서 하면 가시적으로 그래프를 볼 수 있습니다.

NavHost 추가

<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/app_navigation"
/>

Activity에 FragmentContainer를 선언해준 후 위와같이 요소들을 정의합니다.

  • name : NavHost의 구현 클래스 이름을 포함합니다.
  • navGraph : NavHostFragment와 Navigration Graph를 연결합니다.
  • defaultNavHost : 해당 값을 true 로 사용하면 NavHostFragment 가 시스의 뒤로 버튼을 가로챕니다. 하나의 NavHost 만 Default값으로 지정됩니다.

Destination 추가

navigation 의 하위요소로 Destination을 추가해줍니다. activity dialog 도 추가가 가능하지만 fragment 요소만 다룹니다. 아래는 attributes 패널의 속성을 표시합니다.

  • Type : fragment , activity , dialog 로 구현되는지에 대한 여부를 표시
  • Label : 사용자가 읽을 수 있는 Destination 이름을 표시합니다. 예를들어 Toolbar와 함께 사용하는 setupWithNavController() 를 사용할 때 Toolbar의 타이틀이 표시됩니다.
  • ID : 코드에서 Destination에서 참조할 ID를 의미합니다
  • Name : Destination와 연결된 클래스의 이름이 표시됩니다.
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/appNavigation"
app:startDestination="@id/mainFragment">

<fragment
android:id="@+id/mainFragment"
android:name="com.kennethss.android.navigation.ui.MainFragment"
android:label="@string/label_main"
tools:layout="@layout/fragment_main">
</fragment>

</navigation>

navigation요소중에 하위요소중 하나를 선택하여 startDestination으로 정의해줍니다.

Destination 연결

action 은 Destination들간의 논리적인 연결입니다. 이런 연결은 NavGraph에서 화살표로 표시되어 보여줍니다. 일반적으로 action 은 Destintation들 간의 연결이지만 앱의 어느 위치이던 간에 특정 대상으로 이동할 수 있는 Global Action을 만들 수도 있습니다

<fragment
android:id="@+id/mainFragment"
android:name="com.kennethss.android.navigation.ui.MainFragment"
android:label="@string/label_main"
tools:layout="@layout/fragment_main">
<action
android:id="@+id/actionHomeFragmentToSettingFragment"
app:destination="@id/settingFragment"/>
</fragment>
<fragment
android:id="@+id/settingFragment"
android:name="com.kennethss.android.navigation.ui.setting.SettingFragment"
android:label="@string/label_setting"
tools:layout="@layout/fragment_setting">
</fragment>

<action
android:id="@+id/actionGlobalMainFragment"
app:destination="@id/mainFragment"/>

Destination 이동

Destination으로 이동하는 것은 NavController 를 이용해야합니다. NavController 객체는 NavHost에서 앱 탐색을 관리합니다. NavHost는 자신만의 NavController 를 가지고 있습니다.

Kotlin에서는 다음의 메서드를 이용하여 NavController를 찾을 수 있습니다.

FragmentConatinerView를 사용하여 NavHostFragment를 만들 때나 액티비티에서 FragmentTransaction을 통해 NavHostFramgent를 추가할 경우 onCreate() 에서 NavController 를 검색하려고하면 실패합니다. 대신 NavHostFragment 에서 직접 NavController 를 검색해야 합니다.

val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragmentContainer) as NavHostFragment
val navController = navHostFragment.navController

구글에서는 Destination간 이동시에는 간단한 객체나 빌더클래스를 제공하고 Destination간의 argument들의 전달을 도와주는 Safe-Args 사용을 권장하고 있습니다.

가장 상위의 build.gradle 에 다음과 같이 Safe-Args 플러그인을 설치해줍니다.

buildscript {
repositories {
google()
}
dependencies {
def nav_version = "2.5.3"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
plugins {
id 'androidx.navigation.safeargs'
id 'androidx.navigation.safeargs.kotlin' // only kotiin module
}

Safe Args 플러그인을 적용한 후에는 플러그인이 정의된 작업들의 클래스와 메서드를 생성합니다. 생성된 클래스 이름은 해당 Destination의 클래스 이름 + Directions 로 생성됩니다. MainFragment의 생성된 클래스 이름은 예를들어 MainFragmentDirection입니다.

예를들어 MainFragment에서 SettingFragment로연결하는 단일 action 이 있다고 가정했을 때 MainFragmentDirections 클래스를 생성하고 NavDirections 를 반환하는 actionHomeFragmentToSettingFragment() 메서드를 생성합니다. 아래와같이 navigate()NavDirections 객체를 전달할 수 있습니다.

<action
android:id="@+id/actionHomeFragmentToSettingFragment"
app:destination="@id/setting_navigation"
<argument
android:name="id"
android:defaultValue="0" />
</action>
<fragment
android:id="@+id/settingFragment"
android:name="com.kennethss.android.navigation.ui.setting.SettingFragment"
android:label="@string/label_setting"
tools:layout="@layout/fragment_setting">

<argument
android:name="id"
android:defaultValue="0" />
</fragment>

SettingFragment에 전달되는 매개변수를 지정해줍니다.

findNavController(R.id.fragmentContainer)
.navigate(
MainFragmentDirections.actionHomeFragmentToSettingFragment(id = 1)
)
private val args: SettingFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title.text = "Setting ${args.id}"
}

결과

Compose Navigation

위의 Navigation 컴포넌트 구성을 위해 작성한 내용들을 Compose로 구현해보는 방법을 알아보겠습니다.

의존성추가

dependencies {
def nav_version = "2.5.3"

implementation "androidx.navigation:navigation-compose:$nav_version"
}

시작하기

NavController 는 Navigation 컴포넌트의 중심 API이며 NavController 는 Stateful 이며 앱의 각 상태를 가진 화면들을 추적하는 백스택을 추적합니다.
NavController 컴포저블 함수 안에서 rememberNavController() 메서드를 호출하여 생성합니다.

NavHost 만들기

NavHost 를 생성하고 NavController를 연결한다면 Navigation Graph와 Destination간의 이동이 가능한 컴포저블 Destination을 연동하게 됩니다.
만약 컴포저블끼리 탐색이 된다면 NavHost가 자동적으로 리컴포지션을 수행합니다.

NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier
) {
composable(route = "home") {
HomeScreen()
}
composable(route = "my") {
MyScreen()
}
}

Navigation Component를 사용할 때에는 Navigation 원칙을 따라 고정된 시작 Destination을 사용해야합니다. 예를들어 startDestination 에 조건에 따라 다른 화면이 나오게구성해서는 안됩니다.

컴포저블로 이동

NavGraph에서 Destination으로 이동하려면 NavControllernavigate() 함수를 사용하여 Destination의 경로를 나타내는 String 값을 이용하여 컴포저블에서 이동합니다.

navController.navigate("friendslist")

NavController의 navigate 함수는 NavController 의 내부 상태를 수정합니다. 단일 정보 소스 원칙(SSOT)를 준수하려면 하위 UI계층에 있는 컴포저블 함수에서 navigate 함수를 직접 트리거 하는대신에 람다 함수를 이용하여 상위에 선언된 NavController를 포함하고있는 컴포저블함수에서 트리거 되도록 이벤트를 전달해야 합니다.

NavHost(
navController: NavHostController = rememberNavController(),
startDestination = startDestination,
modifier = modifier
) {
composable(route = "home") {
HomeScreen(
onNavigateToMy = { navController.navigate("my") }
)
}
composable(route = "my") {
MyScreen()
}
}

@Composable
fun HomeScreen(
onNavigateToMy: () -> Unit
) {
Button(onClick = onNavigateToMy) {
Text(text = "Move my screen")
}
}

권장사항
구글에서는 이런 이벤트를 람다 매개변수로 노출하는 것이 Singature Function이 오버로드 될 수 있지만 컴포저블 함수의 책임과 가시성이 극대화 되기 때문에 해당 방법을 권장합니다.

HomeScreenEvents 와 같이 컴포저블 함수에서 매개변수를 줄일 수 있는 방법도 있지만 해당 방법은 여러 문제가 있습니다. 첫번째는 컴포저블의 어떤 역할 하는지에 대한 가시성이 감소됩니다. 두번째는 추가되는 스크린마다 클래스와 메서드를 만들어야 하고 적절하게 호출할 클래스 인스턴스를 만들고 기억하기 어렵습니다. 세번째는 래퍼 클래스를 재사용하려면 구글에서 가이드하고 있는 필요한 부분에만 컴포저블에 전달하는 것처럼 하위 컴포저블 계층에 전달하는 방식이 선호되어 탐색 이벤트를 전달하기에는 적절하지 않을 수 있습니다.

인수를 통해 이동

Navigation Compose는 Navigation 컴포저블 함수간의 인수 전달도 지원합니다. 인수를 전달하는 방법은 기존 딥 링크에 인수를 추가하는 방법과 유사합니다

NavHost(startDestination = "setting/{id}") {
composable("setting/{id}") {...}
}

기본적으로 모든 인수는 문자열로 파싱됩니다. 이를 다른 타입으로 지정하기 위해서는 composable() arguments 매개변수에 List<NaedNavArgument> 를 지정해 주면 정확한 type을 만들 수 있습니다

composable(
route = "setting/{id}",
argument = listOf(navArgument("id") {
type = NavType.IntType
defaultValue = 0
})
) {
...
}

이제 NavController 의 navigate 함수를 호출할 때 경로에 추가해주면 됩니다.

navController.navigate("setting/1")

결과

마무리

NavGraph 생성부터 Destination 정의, Destination간의 이동과 매개변수 전달까지 Compose로 Migration 하는 방법을 알아보았습니다. 다음 글에서는 딥 링크를 이용하여 자연스럽게 목적지까지 도달하는 방법을 알아보겠습니다. 긴 글 봐주셔서 감사합니다 🙇‍♂️

Navigation 전체 소스는 아래 링크에서 확인할 수 있습니다.

목차

  1. [Android] Jetpack Navigation — Migrate to Compose — Overview
  2. [Android] Jetpack Navigation — Migrate to Compose — Graph & Destination
  3. [Android] Jetpack Navigation — Migrate to Compose — DeepLink
  4. [Android] Jetpack Navigation — Migrate to Compose — Animation
  5. [Android] Jetpack Navigation — Interaction

참고

--

--

Kenneths
Kenneth Android

사용자들에게 편리하고 AweSome UI, UX를 경험해주고 싶은 상위 티어 개발자가 되고싶어 달려가고있는 개발자입니다. 다양한 내용들을 공유하려고 합니다.