하나의 Activity에서 Fragment만을 교체해가면서 어플을 구성하게 된다면 한번쯤은 Navigation Component를 들어봤을 것이다.
Jetpack에 포함되어 있는 컴포넌트로써 이름처럼 어플 내에서의 흐름을 그래프로써 지정하여 마치 네비게이션처럼 동작한다.
나 역시 Navigation Component를 여러 번 써보았고 여러 이슈를 직면했었다.
내가 왜? Navigation Component를 쓰는지
그리고 어떻게? Navigation Component를 활용했는지 적어두려한다.
기존 네비게이션 방식의 어려운 점들
우선 Navigation Component를 사용하면 여러 화면 (Fragment)를 하나의 Activity로 묶어서 그래프라는 개념으로 관리하게 된다. (Single Activity - Multi Fragment)
Fragment의 전환을 직접 Fragment Manager를 통해 해줘야 했지만, Navigation을 사용하면서 FragmentManager를 통한 화면 전환을 직접 할 필요가 없다. Up & Back에 대한 액션을 네비게이션 컴포넌트에게 위임한다.
기본적으로 navigate() 메소드를 통해 화면전환을 하게 되는데 popUpTo 옵션을 사용하여 특정 위치까지 되돌아갈 수 도 있고 popUpToInclusive 옵션을 통해 목적지 사이에 있는 프레그먼트를 날릴 수도 있다.
SharedElement Animation도 지원한다. 화면 전환이 매우 smooth하게 이루어지는 것.
SafeArgs를 사용하면 자동으로 생성된 코드 클래스를 통해 인자 전달 및 목적지 이동을 안전하게 할 수 있다.
LifecycleOwner로 Fragment를 사용하면 메모리 누수가 발생할 수 있다. LifecycleOwner로 ViewLifecycleOwner를 사용하면 된다.
다음 목적지 인자 전달에는 Bundel || SafeArgs가 가능하지만, 이전 목적지로 돌아가게 되면 인자 전달 기능은 없다. 이는 AAC ViewModel을 사용하여 Activity Scope내에서 인자전달이 가능하다.
NavController.navigate()를 빠르게 2번 호출하면 illegalStateException이 발생한다. graph.xml내의 fragment에 종속된 Action을 Global Action으로 바꾸면 해결된다. 이 경우, Fragment가 2개 생성되는데 이를 RxJava의 debounce 연산자로 중복 이벤트를 방지할 수 있다.
Navigation Component는 크게 명세, 실행 2가지로 이루어져있다.
명세는 Navigation Graph이고 XML로 작성된다.
실행은 NavController 클래스의 navigate() 메소드로 명세를 동작한다.
위의 사진은 많이 본적 있을 것이다. 구글 공식문세에서 설명하는 그림이다.
Navigation Graph는 Destination, Action으로 이루어져 있다.
Destination은 작성해놓은 Activity, Fragment 등의 화면들이다.
Action은 Destination 간 이동 작업의 정의이다.
아래와 같이 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"
app:startDestination="@+id/title_screen">
// Destination
<fragment
android:id="@+id/title_screen"
android:name=".navigationsample.TitleScreen"
android:label="fragment_title_screen"
tools:layout="@layout/fragment_title_screen">
// Action
<action
android:id="@+id/action_title_screen_to_register"
app:destination="@id/register"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
</navigation>
Navigation Component에서의 Activity는 Single로 구현이 된다.
원래 Activity는 화면의 Entry Point이면서도 Content & Navigation Method를 들고 있는 Owner였다.
근데, Navigation Component에서의 Activity는 Entry Point로서의 역할을 해야만 한다!
Content & Navigation Method는 모두 NavHost라는 Fragment에게 위임해야 한다. 그렇기에 Single Activity로 구현이 된다.
// Activity에 NavHostFragment를 두고, Navigation Graph를 연결합니다.
<fragment
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_bottom"
app:navGraph="@navigation/nav_graph_main" />
// Activity는 NavHostFragment를 ActionBar와 연결할 뿐입니다.
// 모든 Contents와 Navigation은 NavHostFragment에서 이루어집니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val navController = findNavController(R.id.nav_host)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp() = findNavController(R.id.nav_host).navigateUp()
}
// findNavController()로 NavController는 받아온다.
// Destination Id를 받는다.
btnGame.setOnClickListener { view ->
view.findNavController().navigate(R.id.gameFragment)
}
// Action Id를 받는다.
btnGame.setOnClickListener { view ->
view.findNavController().navigate(R.id.action_to_gameFragment)
}
// Navigation Class에서 Static Shortcut Method를 제공한다.
btnGame.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_to_gameFragment))
심화된 내용의 경우, 따로 다루려 한다. (이 글에선 너무 길어질 것 같으니)