Jetpack Navigation 에 대해 알아보자! | Android Study

hoya·2022년 8월 17일
1

Android Study

목록 보기
15/19
post-thumbnail

피드백은 언제나 환영합니다 😊
해당 포스팅에 사용된 샘플 코드는 여기를 참고해주세요 :)

🤔 Navigation?

네비게이션화면 이동과 관련된 액션을 쉽게 구현하도록 도와주는 Jetpack 라이브러리 중 하나이다.

기존 Fragment 간 전환 코드를 작성할 때는 Fragment Manager 를 활용할 때가 많았는데, Fragment 백스택에 대한 관리, 전달할 파라미터 등 많은 요소를 코드로 직접 구현해주어야 했기 때문에 컴포넌트의 코드 양이 길어지고 난잡해지는 경우가 많았다.

이런 상황 속에서 Jetpack Navigation 이 등장했고, 기존과 다르게 화면 이동은 물론, 스택 관리, 데이터 전송까지 손쉽게 구현할 수 있게 됐다. 안드로이드 측에서 이야기하는 Navigation 의 장점은 아래와 같다.


  • Fragment 트랜잭션 처리
  • 손쉬운 화면 이동 처리
  • 손쉬운 애니메이션 구현 지원
  • 딥링크 구현 및 처리 지원
  • Safe args 와 함께 안전한 데이터 전달 지원
  • Bottom Navigation, Navigation Drawer 와 같은 요소들을 손쉽게 구현할 수 있도록 지원
  • ViewModel 을 이용, 그래프 내의 컴포넌트 간에 UI 관련 데이터를 공유할 수 있도록 지원
  • 안드로이드 스튜디오의 네비게이션 에디터를 통해 네비게이션 그래프를 한 눈에 보고 쉽게 편집할 수 있도록 지원

Navigation 이 생긴 개요와 장점에 대해 간략히 살펴보았으니, 이제 Navigation 의 구성 요소를 살펴보도록 하자.


Navigation 은 크게 3개의 구성 요소로 이루어진다.

Navigation Graph

  • Navigation 과 관련해 모든 정보를 가지고 있는 구성 요소로, 어떤 목적지들이 있는지, 이동 중 어떤 액션을 취할 것인지, 어떤 데이터를 넘겨줄 것인지에 대한 정보가 담겨져 있다.
  • XML 리소스를 작성할 수도, 코드로 직접 생성할 수도 있으나 보통은 위의 사진과 같이 비쥬얼 에디터를 제공하기 때문에 XML 로 주로 작성한다.

NavHost

  • Navigation Graph 에 담겨져 있는 목적지, 즉 화면을 표현하는 빈 컨테이너 공간이다.
findNavController().navigate(action)

NavController

  • 모든 NavHost 가 개별적으로 가지고 있는 구성 요소로, 이름에 맞게 NavHost 에 어떤 화면을 띄울 것인지 컨트롤하는 역할을 수행한다.

Navigation 을 사용할 때 간단한 플로우는 아래와 같다.

  1. 특정 경로를 따라 이동할지, 특정 목적지로 직접 이동할 것인지 결정 후 NavController 에게 요청한다.
  2. NavControllerNavigation Graph 내에서 적절한 목적지를 찾아 NavHost 에 표현한다.

🙃 실습

기본 설정

dependencies {
    implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
    implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
}

app 레벨의 그래들에 의존성을 추가해준다. 글을 쓰는 시점 (22.08.17) 에서는 2.5.1이 최신 버전이다.


기본 컴포넌트 생성

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">

    <TextView
        android:id="@+id/first_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="@string/first_fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/first_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="@string/button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/first_text" />

</androidx.constraintlayout.widget.ConstraintLayout>

이런 식으로, 텍스트와 버튼이 혼합된 간단한 Fragment 2개를 생성한다.


그래프를 작성하기 위해 res 폴더 내부에 navigation 폴더를 생성하고, 네비게이션 리소스 파일을 생성한다.

위의 GIF와 동일하게, 아이콘을 클릭하여 destination, 즉 목적지를 생성해준다. 현재는 Fragment 2개를 목적지로 생성하였다. 이 상황에서 XML 내부의 코드는 아래와 같다.

<?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/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="co.kr.hoyaho.samplenavi.FirstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" />
    <fragment
        android:id="@+id/secondFragment"
        android:name="co.kr.hoyaho.samplenavi.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />
</navigation>

주의깊게 봐야할 것은 app:startDestination 부분인데, 초기에 보여줄 화면을 지정하는 것이다. 초기에 목적지를 생성하면 자동으로 생성되는 코드라 깊게 걱정하진 않아도 되겠지만, 혹시나 코드에 구성되어 있지 않으면 에러가 발생하니 한번 더 살펴볼 것을 권장한다.

아! 그리고 android:name 에도 해당 화면의 경로를 지정해주어야 한다.


Activity 에서 NavGraph 연결

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/main_container"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        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">

    </androidx.fragment.app.FragmentContainerView>
</androidx.constraintlayout.widget.ConstraintLayout>

이제 Root Activity 를 지정하고, 해당 ActivityNavHost 를 생성해주면 된다. XML 코드 내에 중요 키워드가 몇 가지 있는데, 해당 키워드가 어떤 역할을 하는지 알아보도록 하자.

  • android:name: NavHostFragment 라는 것을 명시, 즉 화면을 담을 빈 컨테이너 역할을 수행할 것임을 알린다.
  • app:defaultNavHost : 위의 NavGraph 에서 명시한 시작점을 백스택에 추가한다. 쉽게 이야기하면, 다른 화면에서 뒤로가기 버튼을 누르면 시작점으로 돌아오게 만든다. (StartDestination -> B -> 백 버튼 -> StartDestination)
  • app:navGraph : 현재 NavHost 와 위에서 지정한 NavGraph 를 연결해주는 역할을 수행한다.

Fragment 에서 다른 Fragment 로 화면 이동

class FirstFragment : Fragment() {

    private var _binding: FragmentFirstBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        _binding = FragmentFirstBinding.inflate(inflater, container, false)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.firstButton.setOnClickListener {
            findNavController().navigate(R.id.secondFragment) // Navigate to another fragment
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

버튼을 클릭했을 때 두 번째 Fragment 로 이동하는 코드이다.

findNavController().navigate() 코드를 통해 NavController 를 탐색하고, 곧바로 이동한다. 정말 손쉽게 화면 이동이 이루어지는 것을 확인할 수 있다.


이동 애니메이션 적용하기

위에서도 이야기했듯이 Navigation이동 중 애니메이션도 쉽게 적용할 수 있도록 도와준다. 스무스하게 오른쪽, 왼쪽으로 이동하는 애니메이션을 적용해보자.

<!-- slide_in_left.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate android:fromXDelta="-100%" android:toXDelta="0%"
               android:fromYDelta="0%" android:toYDelta="0%"
               android:duration="700"/>
</set>

<!-- slide_in_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate android:fromXDelta="100%" android:toXDelta="0%"
               android:fromYDelta="0%" android:toYDelta="0%"
               android:duration="700"/>
</set>

<!-- slide_out_left.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate android:fromXDelta="0%" android:toXDelta="-100%"
               android:fromYDelta="0%" android:toYDelta="0%"
               android:duration="700"/>
</set>

<!-- slide_out_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate android:fromXDelta="0%" android:toXDelta="100%"
               android:fromYDelta="0%" android:toYDelta="0%"
               android:duration="700"/>
</set>

스무스한 이동 애니메이션을 위해 위의 XML 리소스들을 res/anim 폴더에 작성 및 저장한다.

<?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/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="co.kr.hoyaho.samplenavi.FirstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first">

        <action
            android:id="@+id/next_action"
            app:destination="@id/secondFragment"
            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>

    <fragment
        android:id="@+id/secondFragment"
        android:name="co.kr.hoyaho.samplenavi.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" >

        <action
            android:id="@+id/before_action"
            app:destination="@id/firstFragment"
            app:enterAnim="@anim/slide_in_left"
            app:exitAnim="@anim/slide_out_right"
            app:popEnterAnim="@anim/slide_in_right"
            app:popExitAnim="@anim/slide_out_left" />

    </fragment>
</navigation>

다시 NavGraph 리소스로 돌아와, <action> 태그를 작성해주도록 한다.

Action 역시 이름을 보면 알겠지만, 이동 간에 취할 행동을 지정해주는 것이다. 이동할 목적지를 지정해주고, 애니메이션을 넣어주도록 하자.

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.firstButton.setOnClickListener {
            findNavController().navigate(R.id.next_action, null) // 목적지가 아닌 액션을 매개변수로 전달
        }
    }

Fragment 에서는 navigate() 의 매개변수를 바꿔준다. 이 전에는 목적지를 명시해주었다면, 이번에는 액션을 명시해준다.

적용하면 위와 같이 애니메이션이 적용된 것을 확인할 수 있다.


데이터 전달

데이터 전달을 위해 그래들에서 의존성을 추가해준다.

plugins {
    id 'androidx.navigation.safeargs' // java
    id 'androidx.navigation.safeargs.kotlin' // kotlin
}

여기서 끝이 아니고, 또 app 레벨의 그래들에서도 플러그인 세팅을 진행해준다.

plugins {
	id 'androidx.navigation.safeargs.kotlin' version '2.5.1' apply false
}

이제, 위에서 이야기했던 SafeArgs 를 사용할 준비가 완료되었다.


    <fragment
        android:id="@+id/secondFragment"
        android:name="co.kr.hoyaho.samplenavi.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" >

        <argument
            android:name="arg_number"
            app:argType="integer"
            android:defaultValue="5"/>

        <action
            android:id="@+id/before_action"
            app:destination="@id/firstFragment"
            app:enterAnim="@anim/slide_in_left"
            app:exitAnim="@anim/slide_out_right"
            app:popEnterAnim="@anim/slide_in_right"
            app:popExitAnim="@anim/slide_out_left" />

    </fragment>

다시 NavGraph 리소스로 돌아가서, 데이터를 전달받을 목적지에 <argument> 태그를 추가하고, 전달 받을 데이터의 형식을 지정한다.

	// FirstFragment.kt
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.firstButton.setOnClickListener {
            findNavController().navigate(FirstFragmentDirections.nextAction(argNumber = 30))
        }
    }

인자를 전달할 첫 번째 Fragment 의 코드이다. 자세히 보면 navigate 의 매개변수가 Directions 클래스로 수정된 것을 확인할 수 있다.

Directions 클래스는 위의 의존성 추가 과정을 거치면 생성되는 클래스인데, NavGraph 에서 <action> 태그가 있는 화면들에 한해서 생성된다.Directions 클래스를 통해 손쉽게 해당 화면의 <action> 태그에 접근할 수 있다.

그렇게 다음 Fragment 에 인자값으로 30을 넘겨준 모습이다.

	// SecondFragment.kt
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val args: SecondFragmentArgs by navArgs()
        binding.secondNumberText.text = args.argNumber.toString()

        binding.secondButton.setOnClickListener {

            findNavController().navigate(R.id.before_action, null)
        }
    }

인자를 전달받는 Fragment 에서도 SecondFragmentArgs 라는 새로운 클래스가 생성되었는데, 마찬가지로 의존성 추가 과정을 거쳐 생성된 클래스이다. <argument> 태그에 접근할 수 있으며, 받아온 인자값을 사용할 수 있게 해준다.

성공적으로 인자값을 받아온 것을 확인할 수 있다.


그런데, 왜 Safe Arg?

기존에 Fragment 에서 데이터를 전달할 때는, Bundle 을 사용했다.

arguments = Bundle().apply { 
   putString(ItemId, id) 
}

그러나, 이런 방식으로 데이터를 넘길 때 몇 가지 치명적 단점이 존재한다.

  • Key-Value 의 형태로 구성되어 있어 수신자가 Key 값을 잘못 입력하면 데이터를 전달받지 못한다.
  • 모종의 이유로 데이터가 누락됐을 때, 기본값을 지정할 수 없으므로 대응이 불가능하다.
  • 수신자 입장에서 발신자가 무조건 데이터를 보내도록 강제시킬 수 있는 방법도 존재하지 않는다.
  • 위의 코드에서 발신자가 String 형태로 보냈지만, 수신자가 Int 형으로 받는다면 대응할 방법이 없다.

안드로이드 측에서는 이러한 문제를 해결하기 위해 Navigation 에서 Safe arg 를 도입한 것이다.


참고 및 출처

안드로이드 공식 문서
Navigation 훑어보기
Using Safe Args With the Android Navigation Component

profile
즐겁게 하자 🤭

1개의 댓글

comment-user-thumbnail
2024년 1월 31일

유익한 아티클 감사합니다

답글 달기