Navigation은 Android Jetpack의 AAC(Android Architecture Component) 라이브러리 중 하나로 화면 구성 및 UI 전환 을 쉽게 구현할 수 있도록 도와준다.
앱의 build.gradle
에 navigation 라이브러리 의존성을 추가해준다.
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
Navigation graph는 모든 Navigation 관련 정보가 하나의 중심 위치에 모여 있는 XML 리소스이다. 여기에는 destination이라고 부르는 앱 내의 모든 개별적 콘텐츠 영역과 사용자가 앱에서 갈 수 있는 모든 이용 가능한 경로가 포함된다.
2.1. navigation
리소스 폴더 생성
res
폴더에 마우스 우클릭하여 New > Android resource file 을 선택한다.
resource type
을 navigation이라고 지정하고 Navigation 그래프 파일 이름을 작성한다.
2.2. destination을 추가한다.
앞서 생성한 res/navigation/nav_graph.xml
파일을 열고 Design 탭을 클릭한다.
New Destination 아이콘을 클릭하여 화면을 추가한다.
2.3. startDestination을 지정해준다.
<navigation>
태그의 app:startDestination
속성을 반드시 지정해주어야 한다. startDestination으로 지정된 화면은 사용자가 앱을 처음 열 때 기본적으로 실행되는 화면이다.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/movieListFragment">
<fragment
android:id="@+id/movieListFragment"
android:name="com.example.MovieListFragment"
android:label="MovieListFragment" />
</navigation>
NavHost는 Navigation Graph에서 destination을 표시하는 빈 컨테이너이다. 프래그먼트 destination들을 표시하는 NavHost 기본 구현인 NavHostFragment가 포함된다.
여러 Fragment가 있을 때 그것을 담는 그릇을 NavHost라고 볼 수 있다. NavHost는 NavGraph를 알고 있기 때문에 어떤 화면들(대상들)이 있는지 알고 적절하게 화면들을 교체해준다.
NavHostFragment
는 다음과 같이 xml
레이아웃 파일에서 FragmentContainerView
를 통해 구성된다.
app:NavGraph
속성에 NavHost와 연결될 Navigation 그래프를 작성해주면 된다.
<FrameLayout
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/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.movies.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph"/>
</FrameLayout>
NavContoller는 NavHost에서 앱 탐색을 관리하는 객체이다. NavController는 사용자가 앱 내에서 이동할 때 NavHost에서 대상 콘텐츠로의 전환을 관리한다.
4.1. action 추가하여 destination 연결
action은 destination 간의 논리적 연결이다.
Navigation Graph 파일의 Design 탭에서 드래그를 통해 만들 수 있고 화살표로 표시된다.
action을 통해 destination 간의 연결을 설정하면 xml
파일에 다음과 같이 나타난다.
android:id
에 action 자신의 id가 있으며, app:destination
속성에는 fragment 혹은 activity의 id가 포함된다.
<fragment
android:id="@+id/movieListFragment"
android:name="com.shannon.moviemvvm.ui.movies.MovieListFragment"
android:label="MovieListFragment"
tools:layout="@layout/fragment_movie_list">
<action
android:id="@+id/action_movieListFragment_to_singleMovieFragment"
app:destination="@id/singleMovieFragment" />
</fragment>
4.2.NavController 를 활용하여 화면 이동
이제 코틀린 코드에서 아이템을 클릭했을 때 이동하는 코드를 작성한다.
MovieListFragment
안에 있는 RecyclerView
아이템 중 하나를 클릭하였을 때 SingleMovieFragment
로 이동할 수 있도록 클릭 리스너를 추가한다.
그리고 그 안에 NavController
의 navigate()
메소드에서 어디로 갈지 action id를 입력한다.
override fun onSingleMovieClicked(movieId: Int?) {
findNavController().navigate(R.id.action_movieListFragment_to_singleMovieFragment , null)
}
4.3. 이동시 애니메이션 추가
방법1) xml에 추가하는 방법
<action
android:id="@+id/action_movieListFragment_to_singleMovieFragment"
app:destination="@id/singleMovieFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
방법2) 코드에 추가하는 방법
override fun onSingleMovieClicked(movieId: Int?) {
val options = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
findNavController().navigate(
R.id.action_movieListFragment_to_singleMovieFragment , null, options
)
}
방법1) bundle 객체 활용
MovieListfragment
에 아래와 같이 Bundle
객체를 만들고 navigate()
의 파라미터로 넘겨주어 다음 화면에 값을 전달한다.
override fun onSingleMovieClicked(movieId: Int?) {
val bundle = bundleOf("movieId" to movieId)
findNavController().navigate(R.id.action_movieListFragment_to_singleMovieFragment , bundle)
}
전달받는 SingleMovieFragment
에서는 getArguments()
메서드를 사용하여 값을 꺼낸다.
val movieId = arguments?.getInt("movieId") ?: 1
하지만, Bundle 객체를 사용하는 경우 다음과 같은 문제점이 있다.
Type mismatch error
만약 fragment A가 string을 전달했는데 fragment B가 integer를 요청한다면 런타임 에러가 발생할 수 있다.
missing key error
fragment b에서 bundle에 없는 key를 요청한다면 null을 반환하여 런타임 에러가 발생할 수 있다.
방법2) Safe Args 활용
Safe Args 플러그인을 활용하면 화면간 데이터를 주고 받을 때 타입안정성을 보장해준다는 장점이 있다!
Safe Args 플러그인을 추가하기 위해서는 먼저 프로젝트 build.gradle
파일에 다음을 작성한다.
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
그리고 앱의 build.gradle
에 다음을 추가해준다.
plugins {
id 'androidx.navigation.safeargs.kotlin'
}
nav_graph.xml
에 argument
를 추가해준다.
<?xml version="1.0" encoding="utf-8"?>
<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/nav_graph"
app:startDestination="@id/movieListFragment">
<fragment
android:id="@+id/movieListFragment"
android:name="com.shannon.moviemvvm.ui.movies.MovieListFragment"
android:label="MovieListFragment"
tools:layout="@layout/fragment_movie_list">
<action
android:id="@+id/action_movieListFragment_to_singleMovieFragment"
app:destination="@id/singleMovieFragment" />
</fragment>
<fragment
android:id="@+id/singleMovieFragment"
android:name="com.shannon.moviemvvm.ui.details.SingleMovieFragment"
android:label="SingleMovieFragment"
tools:layout="@layout/fragment_single_movie">
<argument
android:name="movieId"
app:argType="integer"
android:defaultValue="1"/>
</fragment>
</navigation>
데이터를 보내는 클래스에서는 (MovieListFragment
) 이름 뒤에 Directions
가 붙은 클래스가 자동으로 생성된다.
데이터를 전달하는 action
도 id
이름 대로 내부에 메소드가 생성이 되어 전달할 값을 넣어주면 된다.
val action = MovieListFragmentDirections.actionMovieListFragmentToSingleMovieFragment(movieId)
findNavController().navigate(action)
데이터를 받는 (SingleMovieFragment
) 에서는 Args
가 붙은 클래스가 자동으로 생성되어 데이터를 꺼내어 쓸 수 있다.
val safeArgs: SingleMovieFragmentArgs by navArgs()
val movieId = safeArgs.movieId
이런 식으로 safe-args
플러그인을 통해 데이터를 주고 받을 때 타입과 키값이 다를 위험으로부터 안전하다고 볼 수 있다.
Navigation 라이브러리가 나온지도 4년이 지났는데 이제야 처음 써보았다. 막상 도입해보니 간편하고 여러 장점들이 있어 그동안 익숙한 방식대로만 개발했던 것을 반성했다. 전체 코드는 아래에서 확인할 수 있다.
https://github.com/sana-20/android-mvvm-coroutine-hilt-sample
[참고자료]
https://developer.android.com/guide/navigation
https://developer.android.com/codelabs/android-navigation?index=..%2F..%2Findex#0