Android Jetpack Navigation 사용법

sana·2022년 6월 30일
0

개요

Navigation은 Android Jetpack의 AAC(Android Architecture Component) 라이브러리 중 하나로 화면 구성 및 UI 전환 을 쉽게 구현할 수 있도록 도와준다.



프로젝트에 적용

1. 환경 설정

앱의 build.gradle 에 navigation 라이브러리 의존성을 추가해준다.

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

2. Navigation graph 만들기

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>

3. NavHost 설정

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>

4. NavController 를 사용하여 화면(destination)간의 이동 설정

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로 이동할 수 있도록 클릭 리스너를 추가한다.
그리고 그 안에 NavControllernavigate() 메소드에서 어디로 갈지 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
       )
    }

5. 화면간 데이터 주고 받기

방법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 객체를 사용하는 경우 다음과 같은 문제점이 있다.

  1. Type mismatch error
    만약 fragment A가 string을 전달했는데 fragment B가 integer를 요청한다면 런타임 에러가 발생할 수 있다.

  2. 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.xmlargument를 추가해준다.

<?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가 붙은 클래스가 자동으로 생성된다.
데이터를 전달하는 actionid 이름 대로 내부에 메소드가 생성이 되어 전달할 값을 넣어주면 된다.

        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

0개의 댓글