Android Jetpack에는 크게 4가지 카테고리가 있습니다. 그 중 아키텍처 카테고리에 속한 컴포넌트 중 하나인 네비게이션 컴포넌트 기본적인 내용을 학습하기 위해 새 프로젝트를 만들고 하나씩 설명하겠습니다.
Navigation Component 란?
네비게이션 컴포넌트는 API이자 앱 내에서 탐색 흐름을 기존보다 훨씬 쉽게 만들고 편집할 수 있게 해주는 안드로이드 스튜디오 디자인 도구입니다.
Navigation Component의 장점
* 프래그먼트 트랜잭션 처리.
* 기본적으로 '위로'와 '뒤로' 작업을 올바르게 처리.
-> AppBar를 사용하는 UI라면 '위로'와 '뒤로' 작업을 기본적으로 동일하게 처리합니다.
백 스택이 없을 경우 '위로'는 앱을 종료하지 않습니다.
* 애니메이션과 전환에 표준화된 리소스 제공.
* 딥 링크 구현 및 처리.
* 최소한의 추가 작업으로 탐색 UI 패턴(예: 탐색 창, 하단 탐색) 포함.
* Safe Args - 대상 사이에서 데이터를 탐색하고 전달할 때 유형 안정성을 제공하는 그래프 플러그인입니다.
* ViewModel 지원 - 탐색 그래프에 대한 ViewModel을 확인해 그래프 대상 사이에 UI 관련 데이터를 공유합니다.
* Android Studio의 Navigation Editor를 사용하여 네비게이션 그래프를 통해 화면 전환 시나리오를 시각적으로 확인하기 쉽습니다.
먼저 제가 사용하고 있는 안드로이드 스튜디오 버전은 4.1.3 버전이고 언어는 Kotlin입니다.
새 프로젝트 만들기
Basic Activity Template를 선택해서 프로젝트를 만듭니다.
Dependency
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
Module 레벨 build.gradle 파일에서 dependency 확인해보면 위에 있는 Navigation 디펜던시를 확인할 수 있습니다.
Safe Args 플러그인 추가
Safe Args 안정적인 매개변수 사용을 위해 쓰이는 플러그인 입니다. 기존에 쓰이던 Bundle을 바로 사용하는 대신 NavArgs를 이용해 키 값의 오타 또는 타입 미스 캐스팅으로부터 안전하게 argument를 사용할 수 있도록 도와줍니다.
Project 레벨의 build.gradle 파일
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" 추가합니다.
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
Module 레벨의 build.gradle 파일
상단에 id 'androidx.navigation.safeargs.kotlin' 추가합니다.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs.kotlin'
}
네비게이션 구성요소
네비게이션 구성요소는 조화롭게 작동하는 세 가지 주요 부분으로 구성됩니다. 주요 부분은 다음과 같습니다.
- NavGraph(새로운 XML 리소스) - 하나의 중앙 위치에 모든 네비게이션 관련 정보가 포함된 리소스입니다. 여기에는 대상이라고 하는 앱의 모든 위치와 사용자가 앱에서 선택할 수 있는 가능한 경로가 포함됩니다.
- NavHostFragment(레이아웃 XML 뷰) - 레이아웃에 추가하는 특수 위젯입니다. 네비게이션 그래프와는 다른 대상을 표시합니다.
- NavController(Kotlin/JAVA 객체) - 네비게이션 그래프 내에서 현재 위치를 계속 추적하는 객체입니다. 네비게이션 그래프에서 이동할 때 NavHostFragment에서 대상 콘텐츠 전환을 조정합니다.
탐색 시 NavController 객체를 사용하여 이동하려는 위치나 네비게이션 그래프에서 선택하려는 경로를 알려줍니다. 그러면 NavController가 NavHostFragment에 적절한 대상을 표시합니다.
activity_main.xml 파일
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.Basic.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.Basic.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
앱이 시작되면 보여지는 메인 액티비티의 레이아웃 파일인 activity_main.xml을 살펴보면 content_main.xml 파일을 불러오는 것을 확인할 수 있습니다.
content_main.xml 파일을 열어 봅니다.
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
<fragment> 태그의 속성을 살펴보면
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
-> 기본 NavHostFragment 로 지정합니다.
app:navGraph="@navigation/nav_garden"
-> 사용할 navGraph 를 지정합니다.
이제 nav_graph.xml 파일로 이동해봅니다.
nav_graph.xml 파일
res/navigation 하위 폴더에 있는 nav_graph.xml 파일에는 해당 네비게이션 그래프가 모든 이동가능한 정보가 들어있습니다.
<navigation> 태그에 app:startDestination 속성은 처음 시작될 프래그먼트를 지정합니다.
<action> 태그에 있는 속성들은 크게 두 가지로 나뉩니다
1. 다음 fragment로 이동하고 이동했을 때 백 스택에 쌓여있는 다른 fragment들을 어떻게 처리할지에 관한 속성
(destination, popUpTo, popUpToInclusive, launchSingleTop)
2. 화면 전환시 애니매이션을 적용하는 속성
(enterAnim, exitAnim, popEnterAnim, popExitAnim)
app:destination 속성
- 대상 fragment로 이동합니다. 백스택에 put() 한다고 생각하시면 됩니다.
--> 즉, destination 속성만 사용해서 a,b,a,b,a,b 이동하는 경우 인스턴스는 a,b 2개가 아니라 6개가 됩니다.
app:popUpTo 속성
- 현재 작업에서 다시 튀어나올 때까지 현재 fragment의 팝핑 옵션을 나타내는 데 사용됩니다.
이 경우 이 목적지를 찾을 때까지 백 스택에서 일치하지 않는 목적지가 팝 됩니다.
--> 이게 무슨 소리냐면 백스택의 경우 스택 자료구조로 되어있는데 a,b,c 순서대로 쌓여 있는 상태에서
a로 popUpTo 할 경우 b와 c는 Stack.pop() 되어서 a만 남는 방식의 동작입니다.
app:popUpToInclusive 속성
- 현재 인스턴스를 포함할지 여부를 지정하는 것입니다
app:LunchSingleTop 속성
- 네비게이션 액션이 싱글톤으로 시작되어야 하는지 여부를 사용합니다(즉, 백 스택 상단에 주어진 목적지의 사본이 기껏해야 한 권 있을 것이다). 이것은 안드로이드.content.Intent.FLAG_ACTIVITY_SINGLE_TOP이 활동과 함께 작동하는 방식과 유사하게 기능합니다.
참고(https://developer.android.com/guide/navigation/navigation-navigate?hl=ko#pop)
app : popUpToInclusive를 사용하지 않으면 fragment 간 자주 이동하면서 백 스택에는 특정 목적지의 두 개 이상의 인스턴스가 포함되어 있습니다.
다른 fragment로 이동하기
<fragment
android:id="@+id/FirstFragment"
android:name="com.practice.jetpack.basic.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
먼저 nav_graph.xml 파일에 first fragment <action> 태그 부분을 살펴보면
app:destination="@id/SecondFragment" 속성을 이용해 이동하려고 하는걸 알 수 있습니다.
FirstFragment.kt 파일에 코드를 보면
view.findViewById<Button>(R.id.button_first).setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
findNavController().navigate(액션_태그_id) 로 지정해서 액션 태그에 정의된 속성대로 화면 전환을 합니다.
Safe Args 이용한 프래그먼트에 인자값을 전달
처음 프로젝트 생성한 후 추가해줬던 Safe Args 플러그인을 이용해서 Fragment간 인자값 전달하는 방법을 알아보겠습니다.
FirstFragment 에서 SecondFragment 로 값을 전달할 겁니다.
SecondFragment 에서 받을 인자 값 설정하기
nav_graph.xml 파일을 열어서 SecondFragment를 선택합니다.
오른쪽 Atrributes 탭에 있는 Arguments 부분의 + 를 누릅니다.
받을 인자값의 이름, 자료형, 기본값을 정한 후 Add 버튼을 누릅니다.
<fragment
android:id="@+id/SecondFragment"
android:name="com.practice.jetpack.basic.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
<argument
android:name="my_lucky_number"
app:argType="integer"
android:defaultValue="0" />
</fragment>
이렇게 <argument> 태그 코드가 추가된 것을 확인할 수 있습니다.
안드로이드 스튜디오 상단 메뉴에 Build>Rebuild Proejct 를 누릅니다.
빌드가 끝나면 Safe Args 플러그인으로 인해 자동 생성된 클래스들을 확인할 수 있습니다.
인자값 넘겨주기
FirstFragment.kt 파일을 엽니다.
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.button_first).setOnClickListener {
val actionFirstFragmentToSecondFragment =
FirstFragmentDirections.actionFirstFragmentToSecondFragment(777)
findNavController().navigate(actionFirstFragmentToSecondFragment)
}
}
}
버튼 클릭시 SecondFragment로 이동하는 자동생성된 FirstFragmentDirections 클래스를 이용해 인자값을 넣어주도록 수정해줍니다.
인자값 받기
SecondFragment.kt 파일을 엽니다.
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class SecondFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val safeArgs: SecondFragmentArgs by navArgs()
val luckyNumber = safeArgs.myLuckyNumber
Toast.makeText(context, "luckyNumber = $luckyNumber", Toast.LENGTH_SHORT)
.show()
view.findViewById<Button>(R.id.button_second).setOnClickListener {
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
}
}
}
자동생성된 SecondFragmentArgs 클래스를 이용해서 FirstFragment로부터 전달된 인자값을 받습니다.
val safeArgs: SecondFragmentArgs by navArgs()
val luckyNumber = safeArgs.myLuckyNumber
by navArgs() 위임된 프로퍼티를 사용합니다.
이처럼 Safe Args 플러그인으로 인해 자동생성된 클래스에서 명확한 타입과 이름을 사용하기 때문에 안전하고 명확하게 인자값을 주고 받을 수 있습니다.
지원되는 인수 유형
이상 기본적인 네비게이션 사용방법을 알아보았습니다~!