Navigation 훑어보기

가능한 것과 안되는 것을 간단히 알아보자

Sungyong An
20 min readMar 10, 2019

Navigation Architecture Component(이하 Navigation)는 18년도에 처음 소개된 Android Jetpack Library입니다. Fragments, Activities와 같은 components 간 탐색을 돕기 위한 framework으로, 최근 2.0.0-rc02로 버전업되었습니다. 정식 버전이 코앞에 다가온 느낌이라, 관심을 가지시는 분들이 많아지는 것 같은데요. Navigation이 어떤 것을 기본으로 제공하고, 어떤 것은 제공하지 않는지를 간단히 알아보려 합니다.

시작하기 전에…

기본적으로 Navigation은 Single Activity — Multiple Fragments 구조에 대한 지원이 대부분입니다. 그래서 Activity 탐색을 주로 다루는 경우 이점이 적습니다. Conductor 같은 View-based Architecture 인 경우에도 사용에 적합하지 않아 보입니다. 그리고 Navigation이 Fragment 탐색을 돕는 것에 초점이 있어서인지 ActivityResult와 같은 Result 송/수신 기능이 제공되지 않습니다. (Fragment API에 제공되지 않는 기능이라 그런듯 합니다.)
따라서 아직은 부족한 점이 다소 있어 보이는데요. 이점은 유념하시고 봐주세요. :)

제공하는 것들 (Link)

Navigation 사용하면 이런 것들을 할 수 있습니다.
Fragment transaction을 직접 다루지 않아도 되며, Back/Up 동작을 기본 지원합니다. 화면 전환에 Type이 지정된 데이터 전달이 가능하고, 안드로이드 리소스로 Animation/Transition 설정이 가능합니다. 딥링크 지원합니다. Navigation drawer, bottom navigation 같은 친구들에 Navigation을 쉽게 연동할 수 있습니다. Android Studio 3.3 이상부터는 비주얼 편집기인 Navigation Editor가 제공됩니다.

기본원칙 (Link)

Navigation은 아래와 같은 기본 원칙이 있습니다. 이 부분은 설계원칙과 같은 것이니, 꼭 원문을 읽어보시길 추천드립니다.

  • 항상 고정된 시작 위치에서 시작해야 한다.
  • Navigation 상태는 Destination의 Stack으로 표현되어야 한다.
  • Up 버튼으로 앱이 종료되면 안된다.
  • App Task 내에서는 Up과 Back 버튼은 동일하게 동작해야 한다.
  • 딥링크와 탐색은 동일 Stack으로 처리되어야 한다.

Install (Link)

프로젝트 기반 언어(Java or Kotlin)에 따라 다르게 사용할 수 있습니다.
Kotlin 용 라이브러리를 사용하면 Extension function을 추가 지원해주므로, 가능하다면 Kotlin을 사용할 것을 권해드립니다.

// For Java
"androidx.navigation:navigation-fragment:$version"
"androidx.navigation:navigation-ui:$version"
// For Kotlin
"androidx.navigation:navigation-fragment-ktx:$version"
"androidx.navigation:navigation-ui-ktx:$version"
/* SafeArgs (Optional) */classpath
"androidx.navigation:navigation-safe-args-gradle-plugin:$version"
// For Java
apply plugin: "androidx.navigation.safeargs"
// For Kotlin
apply plugin: "androidx.navigation.safeargs.kotlin"

3 Major Components (Link)

NavGraph, NavController, NavHost이 3가지가 Navigation을 구성하고 있습니다. NavGraph는 Destination 목록을 갖고 있는 녀석, NavControllerNavGraph를 다루는 녀석, NavHostNavController를 갖고 있는 컨테이너라고 생각하시면 됩니다.
아래에 Navigation 구성을 간단히 그림으로 그려보았는데요.
A화면에서 B화면으로 이동을 원한다고 생각해봅시다. 각 화면은 NavDestination으로 매치되고, 각각 다음 NavDestination을 요청할 수 있는 NavDirections도 갖습니다. NavDirectionsNavController에 요청하면 — 내부적으로 Stack을 처리하고 — 다음 NavDestination을 실행하게 됩니다. 이 때, 전달되어야 할 데이터가 있으면 NavArgs에 담아 전달합니다.

Navigation Interface Overview
FragmentNavigator / ActivityNavigator / NavGraphNavigator

NavGraph

NavGraph는 XML 작성하거나 코드로 직접 생성할 수 있습니다. 다만 Visual Editor가 제공되므로 XML을 사용하는 것이 훨씬 편리합니다. 아래는 navigation XML에서 사용할 수 있는 기본 태그입니다. 참고로 Navigation XML은 res/navigation/ 폴더에 추가해주어야 합니다.

  • <navigation> : NavGraph
  • <fragment> or <activity> : NavDestination
  • <action> : NavAction (handled in NavDirections)
  • <argument> : NavArgs

아래는 (Fragment 2개와 Activity 1개를 갖는) 간단한 NavGraph 예제입니다.
이를 기반으로 Navigation이 생성해주는 코드를 살펴 볼 예정입니다.

NavDirections

NavDirections는 Activity 실행과 비교하면 Intent와 비슷합니다.
NavDirections는 개발자가 직접 작성하는 것은 아니고, Navigation에 의해 자동 생성되는 클래스입니다.

코드로 살펴봅시다. 아래 action이 Directions 클래스로 생성됩니다.

// NavGraph에 선언된 코드
<fragment
android:id="@+id/second"
android:name="생략.SecondFragment" >
<action
android:id="@+id/action_to_third"
app:destination="@id/third" />
</fragment>

다음 목적지로 향하는 Action ID와 전달될 Arguments들을 Bundle에 담아주는 형태입니다. SecondFragmentDirections.actionToThird("This is a label")처럼 사용하면 됩니다.

참고로 Java에서는 @Nullable, @NonNull Annotation만 달라지고, Kotlin에서는<arguments>nullable 속성에 따라 ? 표기가 달라집니다. (Kotlin 추천!)

다음으로 넘어가기 전에 Action의 Scope를 잠깐 얘기하겠습니다.
NavAction은 Global / Local로 사용할 수 있습니다. 특정 Destination 태그 내에 쓰여진<action>는 Local Action이 되고, <navigation> 태그 바로 하위에 선언된 <action>은 Global Action이 됩니다. Local Action은 해당 Destination에서만 사용할 수 있고, Global Action은 어떤 Destination에서든 사용할 수 있습니다.
예제 NavGraph에서 예를 든다면, id/action_to_second는Global Action, id/action_to_third는 SecondFragmentDirections에만 추가되는 Local Action이 됩니다.

SafeArgs (NavArgs)

NavDirections에서 언급하지 않았지만, 다음 화면에 값을 전달하려면 SafeArgs Plugin을 설치해야 합니다. 실질적인 값 전달은 Android Platform을 이용하기 때문에 NavArgsBundle에서 NavGraph 선언과 동일한 Type Data를 가져오는 Helper로 생각하시면 될 것 같습니다.

NavArgs 클래스를 아래처럼 사용하시면 됩니다.

// Activity
intent?.extras?.let {
ThirdActivityArgs.fromBundle(it).label // Nullable
}
// Fragment
arguments?.let {
SecondFragmentArgs.fromBundle(it).data // Non-Null
}

참고로 NavArgs 클래스는 아래와 같은 형태로 생성됩니다. (Kotlin Plugin)
Java Plugin은 조금 다르게 생성되지만 핵심은 동일합니다.

Kotlin Plugin— navArgs()
1.0.0-alpha10 부터 추가된 기능입니다.
Nullable Bundle을 Non-null처럼 다룹니다.
(Bundle이 유효하지 않은 경우, IllegalStateException이 발생됩니다.)

Kotlin users can now use the by navArgs() property delegate to lazily get a reference to a Safe Args generated NavArgs class in an Activity or Fragment. b/122603367

아래처럼 사용할 수 있습니다. (?가 줄어들어 행복합니다.)

// Activity
import androidx.navigation.navArgs
private val args by navArgs<ThirdActivityArgs>()
// Fragment
import androidx.navigation.fragment.navArgs
private val args by navArgs<SecondFragmentArgs>()

NavDirections, NavArgs 생성에서 재미있는 점

NavDirections, NavArgs 코드는 NavDestination Type에 영향받지 않습니다. (즉, Type에 상관없이 동일한 형태로 생성됩니다.)
예를 들어, 아래처럼 제공되지 않는 태그(<dialog>)를 선언하더라도 빌드 오류가 발생하지 않습니다. (ThirdDialogFragment라는 이름만 사용합니다.)

<navigation>
<!-- If use unknown tags, what happend? -->
<dialog android:name="생략.ThirdDialogFragment">
<argument
android:name="test"
app:argType="String" />
</dialog>
</navigation>

NavController

다음 화면을 요청할 준비(NavDirections)가 되었고, 데이터를 받을 준비(NavArgs)도 되었습니다. 두 가지를 연결시켜 줄 NavController에는 현재 아래와 같은 API가 제공되고 있습니다.

// 앞으로 가기
void navigate(int resId)
void navigate(int resId, Bundle args)
void navigate(int resId, Bundle args, NavOptions navOptions)
void navigate(int resId, Bundle args, NavOptions navOptions,
Navigator.Extras navigatorExtras)
void navigate(NavDirections directions)
void navigate(NavDirections directions, NavOptions navOptions)
void navigate(NavDirections directions,
Navigator.Extras navigatorExtras)
// 뒤로 가기
boolean navigateUp()
boolean popBackStack()
boolean popBackStack(int destinationId, boolean inclusive)
// 탐색 변경 리스너
interface OnDestinationChangedListener
void addOnDestinationChangedListener(
OnDestinationChangedListener listener)
void removeOnDestinationChangedListener(
OnDestinationChangedListener listener)

다만 Activity 실행과 비교해보면 차이점이 있습니다.

+-----------------------------------------+----------+------------+
| 구분 | Activity | Navigation |
+-----------------------------------------+----------+------------+
| 다음 화면으로 이동하기 | O | O |
| 현재 화면 없애고, 다음 화면으로 이동하기 | O | O |
| 특정 목적지까지 되돌아간 후, 다음 화면으로 이동하기 | X | O |
+-----------------------------------------+----------+------------+
+--------------------------------+----------+------------+
| 구분 | Activity | Navigation |
+--------------------------------+----------+------------+
| 바로 전으로 되돌아가기 | O | O |
| 바로 전으로 되돌아가기 (+ Result) | O | X |
| 특정 목적지까지 되돌아가기 | X | O |
| 특정 목적지까지 되돌아가기 (+ Result) | X | X |
+--------------------------------+----------+------------+

여기서 자세하게 다루진 않겠지만, SharedElements나 Activity Flag 같은 기능도 동일하게 제공됩니다.

NavHost

NavController 사용법도 알게 되었지만 뭔가 허전합니다. NavController는 어떻게 참조할 수 있을까요? NavControllerNavHost가 들고 있습니다. 하지만 NavHost는 interface 입니다. 구현체가 필요합니다. 중간에 작은 그림을 보셨다면 눈치채셨을 수도 있는데요. Navigation은 Single Activity — Multiple Activity에 친화적입니다. NavHostFragment 클래스를 제공하는데 이것을 Activity의 Root에 두고, Child Fragments를 탐색하는 형태로 구현되어 있습니다. 아래와 같은 코드를 사용해야 합니다.

그럼 Activity만 쓰는 경우에는 어떻게 할까요? 아쉽게도 현재는 지원되지 않습니다. 다만 아래 답변처럼 Multiple Activities에서 각 Activity마다 따로 NavGraph를 관리하는 방법도 가능합니다.

https://stackoverflow.com/a/50452715/3264600

The navigation graph only exists within a single activity. As per the Migrate to Navigation guide, <activity> destinations can be used to start an Activity from within the navigation graph, but once that second activity is started, it is totally separate from the original navigation graph (it could have its own graph or just be a simple activity).

Navigation (class)

Navigation 클래스는 NavHost를 쉽게 찾을 수 있도록 돕는 클래스입니다. Kotlin을 사용하면 extension function도 제공됩니다.

static View.OnClickListener createNavigateOnClickListener(int resId)
static View.OnClickListener createNavigateOnClickListener(int resId, Bundle args)
static NavController findNavController(
Activity activity, int viewId)
static NavController findNavController(View view)
static void setViewNavController(
View view, NavController controller)

지금까지의 내용을 코드로 사용하면, 아래와 같습니다. :)

// SecondFragment.kt
class SecondFragment: Fragment() {
fun goToNextDestination() {
findNavController().navigate(
SecondFragmentDirections.actionToThird("This is a label"))
}
}
// ThirdActivity.kt
import
androidx.navigation.navArgs
class ThirdActivity: Activity() { private val args by navArgs<ThirdActivityArgs>() override fun onCreate(savedInstanceState: Bundle?) {
...
args.label // "This is a label"
}
}

NavigationUI (class)

Navigation 패턴에 관련된 Material Components와 Navigation을 연결하기 쉽게 도와주는 Helper 클래스입니다. 다만 제공되는 항목들과 연계하여 사용해보면 아직은 많이 아쉬운 느낌이 듭니다.

Navigator

NavController 내부적으로 NavDestination을 다룰 때, Type에 따라 세부적인 제어를 Navigator 클래스에 위임하고 있습니다. 참고로 하나의 Navigator는 하나의 NavDestionation Type에 연결됩니다.
아래는 기본 제공되는 Navigator입니다. NavHostFragment를 사용하면 3가지 모두 사용할 수 있습니다.

Custom Navigator (Link)
새로운 NavDestination을 사용하고 싶다면, Custom Navigator를 만들 수 있습니다. 기본 Navigator도 아래와 같은 형태로 정의되어 있으므로, 구현만 잘 하면 동일 Interface로 다양한 처리를 할 수 있을 것 같습니다.

@Navigator.Name("activity")
public class ActivityNavigator extends
Navigator<ActivityNavigator.Destination> {
...
@NavDestination.ClassType(Activity.class)
public static class Destination extends NavDestination { ... }
}

아래는 Custom Navigator 구현에 참고할 만한 Github을 소개합니다.

1. DialogNavigator
: Navigation으로 DialogFragment를 다룹니다.

2. KeepStateNavigator (BottomNavigationView + NestedFragments)

: BottomNavigationView으로 탭 전환 시에도 Nested Fragments의 Stack을 유지하려는 목적입니다. 하지만 Nested와 Parent에서 다른 NavController를 사용해야 한다는 부분이 아쉬운 부분입니다. (Link)

3. ControllerNavigator
:
Conductor의 Navigation 버전입니다.

두서없는 설명이 많이 있는데도 읽어주셨다면 감사합니다. 이번 글은 간단히 Navigation을 훑는 정도에서 멈췄는데요. 다음 글에서는 내부 코드를 분석해보거나, 응용하여 여러가지 앱 구조를 구현해보는 것으로 올릴 생각입니다.
현재 아래 Github Repository에 여러가지 Navigation 예제 코드를 만드는 중인데요. 필요한 내용이 있다면 이슈를 등록하거나, 이글에 댓글 남겨주세요.

--

--