Computer >> 컴퓨터 >  >> 시스템 >> Android

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드

애플리케이션을 설계하는 일은 생각보다 번거롭습니다. 흔히 화이트보드에는 여러 지점 사이를 잇는 화살표들이 가득 그려져 있죠.

처음에는 액티비티 한두 개면 충분할 것이라 생각했던 앱이 어느새 여러 개의 플로우와 프래그먼트, 그리고 다양한 사용자 인터랙션으로 복잡해지곤 합니다. 화이트보드에 그려진 설계를 코드로 손쉽게 옮겨올 수 있다면 얼마나 좋을까요?

바로 내비게이션 컴포넌트(Navigation Component)의 등장입니다.

처음 접하는 분들을 위해 말씀드리면, 내비게이션 컴포넌트는 액티비티나 프래그먼트의 레이아웃 대신 사용하는 또 다른 UI 클래스가 아닙니다. 비유하자면 하나의 '지도'라고 생각하면 됩니다. 대륙 대신 프래그먼트들이 자리하고 있고, 대륙에서 대륙으로 이동하려면 방향(directions)이 필요한 것이죠. 내비게이션 컴포넌트는 프래그먼트와 그들 간의 연결 관계를 위에서 아래로 내려다보는 형태로 시각화해 줍니다. 이 글에서는 이 컴포넌트의 핵심 개념을 살펴보고, 실제 애플리케이션에 통합하는 방법까지 알아보겠습니다.

항해할 준비 되셨나요? ⛵️

1. 기본 설정 다지기

내비게이션 컴포넌트는 Android Studio 3.3 이상부터 사용할 수 있습니다. 프로젝트에 아래 의존성을 추가하세요.

android {
    ...
}

dependencies {
    implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'
}

실습을 위해 다음과 같은 구조의 애플리케이션을 설계했다고 가정해 봅시다.

  • Start Fragment
  • Fragment A
  • Fragment B

사용자는 Start Fragment에서 Fragment A 또는 Fragment B로 이동할 수 있습니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
우리가 만들 애플리케이션 구조

만약 내비게이션 컴포넌트 없이 이 모든 것을 구현한다면, 버튼 클릭 시 프래그먼트를 여는 익숙한 코드를 직접 작성해야 할 것입니다.

val myFragment : MyFragment = MyFragment()
supportFragmentManager.beginTransaction().add(R.id.container, myFragment).commit()

간단한 예제에서는 몇 줄 안 되는 코드라 크게 부담스럽지 않지만, 앱의 규모가 커지고 사용자 플로우가 복잡해지면 이런 방식은 확장성 측면에서 한계에 부딪힌다는 점에 모두 동의할 겁니다.

2. 내비게이션 그래프 만들기

내비게이션 컴포넌트를 사용하려면 먼저 내비게이션 그래프(navigation graph)를 생성해야 합니다. 이 그래프는 앞서 말한 '지도' 역할을 하며, 앱의 사용자 플로우를 정의합니다. 생성하려면 res 폴더를 우클릭해 새 리소스 파일을 만들고, 파일 이름을 user_flow_graph.xml로 지정한 뒤 파일 타입을 반드시 Navigation으로 선택하세요.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
새 리소스 파일 생성

모든 항해에는 출발 항구가 필요하듯, 내비게이션에도 시작점이 필요합니다. 그 역할을 하는 것이 바로 NavHost입니다. NavHost는 사용자가 UI와 상호작용할 때 목적지(destination)가 교체되어 표시되는 자리표(placeholder) 역할을 합니다. 액티비티의 메인 레이아웃에 NavHost를 추가해 봅시다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/user_flow_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

프래그먼트들이 표시되고 교체될 공간을 담당하는 fragment 엘리먼트를 추가했습니다. 특히 navGraph 속성에 주목하세요. 앞서 생성한 XML 파일과 연결되는 부분입니다.

이제 시작 목적지(start destination)를 추가해야 합니다. 지정하지 않으면 프로젝트가 빌드되지 않습니다.

user_flow_graph.xml을 연 상태에서 내비게이션 에디터의 작은 + 아이콘을 클릭합니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
목적지 추가하기

팝업 메뉴를 보면 나중에 채워 넣을 플레이스홀더를 만들거나, 이미 있는 프래그먼트 중에서 선택할 수 있습니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
목적지 후보들

사용자 플로우는 Start Fragment에서 시작되므로, 이것부터 선택하겠습니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
출발 항구 (작은 집 아이콘이 보이시죠?)

이어서 나머지 두 프래그먼트인 Fragment A와 Fragment B도 추가합니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
모든 목적지 추가 완료

목적지에 마우스를 올리면 나타나는 점(dot)을 클릭한 뒤 다른 목적지로 드래그하면 두 목적지가 연결됩니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드
꽤 깔끔하죠?

방금 Start Fragment와 Fragment A, B 사이에 만든 것이 바로 액션(action)입니다.

3. 코드로 화면 이동 구현하기

'목적지를 선으로 연결하기만 하면 알아서 마법처럼 작동하지 않을까?'라고 궁금해하셨을 수 있습니다.

안타깝게도 그렇지 않습니다. 코드에 특정 목적지로 이동하라고 직접 명령해 주어야 하죠. 어떻게 할까요? 물론 이 과정에도 약간의 마법이 숨어 있습니다.

먼저 Safe Args라는 Gradle 플러그인을 추가합니다. 이 플러그인은 목적지 간 이동 시 타입 안전성(type safety)을 보장해 줍니다.

buildscript {
   /...
    }
    dependencies {
        ...
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0"
        
    }
}

그리고 앱 모듈의 build.gradle에 다음 플러그인도 추가합니다.

apply plugin: "androidx.navigation.safeargs.kotlin"

또한 gradle.properties 파일에 android.useAndroidX=true가 설정되어 있는지 확인하세요.

왜 이런 설정들이 필요한지 짚고 넘어가겠습니다. 기본적으로 앞서 액션을 생성하면, 그 뒤편에서 Android Studio가 액션을 실행하는 데 사용할 코드를 자동으로 생성합니다. 이 코드는 각 액션을 표현하는 메서드와 클래스로 구성됩니다. Start Fragment를 예로 들어볼까요? 선언한 액션에 대해 생성된 코드에는 StartFragmentDirections라는 클래스가 포함되며, 이 클래스의 메서드가 바로 우리가 만든 액션들입니다. 즉 두 프래그먼트에 대해 다음 메서드들이 생깁니다.

  • StartFragmentDirections.actionStartFragmentToFragmentA()
  • StartFragmentDirections.actionStartFragmentToFragmentB()

액션이 코드로 변환되었으니 이제 사용해 봅시다.

val action = StartFragmentDirections.actionStartFragmentToFragmentA()

마지막 단계는 NavController를 활용하는 것입니다. 이 객체는 NavHost 내부의 내비게이션을 관리하는 역할을 담당하며, 다음 세 가지 방법 중 하나로 접근할 수 있습니다.

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: Int)

지금까지의 내용을 조합하면 다음과 같은 코드가 완성됩니다.

fragmentABtn.setOnClickListener { button ->
    val action = StartFragmentDirections.actionStartFragmentToFragmentA()
    button.findNavController().navigate(action)
}

4. 목적지 간 데이터 전달하기

목적지 사이에 데이터를 전달하려면 어떻게 해야 할까요? 예를 들어 사용자가 특정 아이템을 클릭했을 때, 다음 화면에서 해당 아이템을 가지고 무언가 처리해야 하는 상황을 상상해 보세요. 이를 위해 목적지 인수(destination arguments) 기능이 제공됩니다.

user_flow_graph.xml을 열고 Fragment A를 클릭하면, 오른쪽에 Fragment A의 다양한 속성을 보여주는 패널이 나타납니다. 그중 Arguments 항목이 있을 겁니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드

➕ 아이콘을 클릭하면 팝업 창이 열리고, 여기서 인수를 구성할 수 있습니다. 이름을 지정하고 타입을 선택하며 기본값을 추가할 수 있죠. Start Fragment에서 전달받은 메시지를 저장할 String 타입 인수를 Fragment A에 추가해 보겠습니다.

Android 내비게이션 컴포넌트(Navigation Component) 구축 방법 완벽 가이드

Start Fragment에서 액션을 정의하고 생성된 메서드를 호출하는 부분에 인수를 함께 넘겨주면 됩니다.

fragmentABtn.setOnClickListener { button ->
    val action = StartFragmentDirections.actionStartFragmentToFragmentA("Hello From Start Fragment")
    button.findNavController().navigate(action)
}

Fragment A에서 이 값을 읽는 방법은 두 가지입니다.

첫 번째, Bundle에 접근해 message 값을 가져오는 방식입니다.

class FragmentA: Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val bundle = arguments
        val root = inflater.inflate(R.layout.fragment_a, container, false)
        val textView : TextView = root.findViewById(R.id.textView)
        textView.text = bundle?.getString("message")
        return root
    }
}

두 번째, -ktx 의존성을 사용 중이라면 navArgs를 활용하는 방식입니다.

class FragmentA: Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val args : FragmentAArgs by navArgs()
        val root = inflater.inflate(R.layout.fragment_a, container, false)
        val textView : TextView = root.findViewById(R.id.textView)
        textView.text = args.message
        return root
    }
}

✋ navArgs를 사용할 때는 build.gradle 파일에 Java 8 지원을 추가해야 한다는 점을 잊지 마세요.

이 글에서 소개한 전체 코드는 GitHub 저장소에서 확인하실 수 있습니다.