시기상으로 너무 늦게작성되는 포스팅인데… 지금 시점이라면, Jetpack Compose에서 Navigation의 사용을 설명해야 하나, 내가 아직 잘 모르고 XML도 혼용해서 사용중이라서 XML사용하는 Fragment들간의 Navigation을 설명하는 포스팅을 올린다.
Principles of Navigation
Navigation의 구현 이전에, 지켜져야 하는 디자인 원리를 짚고 넘어가자. 다음의 Principles of Navigation은 Navigation component를 쓰지 않더라도 지켜져야 한다. Navigation component를 사용하는 경우 지원되는 내용이기도 하다.
시작 데스티네이션은 항상 고정이다. (Fixed start destination)
런처에서 앱 실행시, 첫화면은 고정된 destination이어야 한다. back key로 돌아갈 때, 앱 종료전 마지막 화면도 이 화면이어야 한다.

네비게이션 상태는 데스티네이션들의 스택으로 표현된다. (Navigation state is represented as a stack of destinations)
Navigation은 destination들의 back stack으로 이루어진다.
업 버튼과 백 버튼은 앱 내에서 동일한 동작을 한다. (Up and Back are identical within your app’s task)

app bar에 보이는 Up 버튼은 단일 앱 task에서 시스템 back 버튼과 동일한 기능을 갖는다. back 버튼은 back stack의 최신 destination을 꺼내고 back stack의 이전 destination으로 돌아간다.
업 버튼은 앱을 종료시키지 않는다. (The Up button never exits your app)
앱의 시작 destination으로 이동하면, Up 버튼은 보이지 않아야 한다. Up 버튼과 Back 버튼의 차이인데, 만약 다른 앱에서 deep link로 시작이 됐다면, Up 버튼은 현재 앱에 가상의 back stack을 생성해 이동할 것이다. 이 앱을 실행시킨 다른 앱으로 돌아가지 않는다는 얘기. 하지만, 시스템 back 버튼은 이 앱을 실행시킨 이전 앱으로 돌아갈 것이다.
딥링크는 앱을 수동으로 사용했을 때를 흉내내야 한다.(Deep linking simulates manual navigation)
사용자가 직접 예제인 sunflower앱을 실행해 apple 화면을 보고 있었다면 back stack은 다음과 같이 생성될 것이다.

사용자가 Home 버튼을 눌러 빠져나간 후, deep link를 이용하여 특정 식물화면으로 바로 이동을 한다면, 다음과 같이 back stack이 완전히 교체된다.

이렇게 생성되는 back stack은 실제 사용자가 직접 해당 destination으로 이동하는 과정과 동일해야 한다.
네비게이션의 구현
예제를 작성하기위해 새 프로젝트를 만들고 Basic View Activity를 선택했다. 기본적으로 Fab 버튼이 달린 템플릿이 작성된다. 또한 기본적으로 navigation이 적용되어 있다.
gradle 파일에 navigation 컴포넌트 추가
dependencies {
...
implementation("androidx.navigation:navigation-fragment-ktx:2.7.3")
implementation("androidx.navigation:navigation-ui-ktx:2.7.3")
위 코드는 기본생성된 코드이다. 여기서 구현하는데에는 이걸로 충분하지만, 공식 가이드 문서를 따르면 다음을 추가할 수 있다.
// Feature module Support
implementation("androidx.navigation:navigation-dynamic-features-fragment:2.7.3")
// Testing Navigation
androidTestImplementation("androidx.navigation:navigation-testing:2.7.3")
// Jetpack Compose Integration
implementation("androidx.navigation:navigation-compose:2.7.3")
테스팅이나 Compose를 지원하기 위한 부분으로 여기서는 언급만 해둔다.
navigation graph를 저장하는 nav_graph.xml 리소스 추가.
navigation은 navi graph를 통해서 생성할 수 있다. navi graph는 화면간 이동을 그래프로 그린 것인데, 기본적으로 res>navigation>nav_graph.xml 을 생성해 만들 수 있다. 위에서와 같이, 기본 프로젝트를 만들었다면 자동으로 생성되어 있을 것이다.
xml인 만큼, 기본적으로 xml 텍스트로 기술되어 있다. 하지만, android studio에선 보다 직관적인 그래프 형태의 에디터를 제공한다. 공식 가이드 문서를 참고해보면, 에디터를 다음과 같이 설명하고 있다.

- Destination Panel : navigation host 목록과 현재 에디터에서 쓰이는 모든 destination들의 목록표시
- Graph Editor : 네비게이션 그래프의 시각적 표현 편집기.
- Attributes : 선택된 아이템의 속성들
layout과 마찬가지로, Design뷰를 선택하면 위와같은 그래프를 볼 수 있고, Code를 선택하면 XML 파일을 보며 편집이 가능하다.
nav_graph.xml은 다음과 같이 navigation 태그로 묶인다.
<?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">
</navigation>
메인 레이아웃에 navHost 설정.
가장 중요한 부분중 하나인데, navigation이 작동하려면 navigation host를 설정해야 한다. navigation host라는건 빈 컨테이너로, navigation이 작동함에 따라 지정된 destination을 바꿔가며 표시해주는 역할을 한다.
<?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.appcompat.widget.Toolbar
.../>
<androidx.fragment.app.FragmentContainerView
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/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
위 MainActivity 의 레이아웃 xml에 들어가 있는 FragmentContainerView가 navigation host역할을 한다. 이 자리에 destination이 되는 Fragment들이 바꿔치기(swap)되면서 들어오게 된다.
FragmentContainerView를 보면, android:name=”androidx.navigation.fragment.NavHostFragment” 로 NavHost 인터페이스를 구현한 제공되는 클래스이다. 편의상 제공되는걸 사용했지만, 뭐가 됐든 NavHost인터페이스를 구현한 클래스를 사용하면 된다. 태그 마지막엔, app:navGraph=”@navigation/nav_graph”로 앞에서 작성한 navigation graph xml파일을 지정하고 있다. 마지막으로, app:defaultNavHost=”true” 로 지정함으로서, 시스템의 백버튼 기능을 인터셉트한다. 이 디폴트 네비 호스트는 여러개의 네비 호스트를 사용하더라도 단 하나만 지정이 가능하다.
Navigation graph editor의 사용

왼쪽의 아이콘을 클릭해서 destination추가가 가능하다. 아이콘을 클릭하면, Activity, Fragment 그리고 더미로 사용가능한 placeholder가 표시되며, 이중에 하나를 사용할 수 있다.
위와같이 destination을 추가하면, Attributes 패널에서 속성들을 입력할 수 있다. 이 값들은 nav graph xml파일에 다음과 같이 추가된다.
...
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="@string/label_blank"
tools:layout="@layout/fragment_blank" />
...
start destination은 지정할 destination을 선택한 후, 집모양 아이콘을 클릭하거나, 선택한 destination에서 마우스 우클릭> Set as Start Destination을 이용해 지정가능하다. 이 포스팅의 제일 처음, Principle of Navigation에서 앱은 고정된 Start Destination을 가져야 한다고 말했는데, 바로 그것, 앱의 시작화면을 설정하는 것이다.
이렇게 destination들을 추가하고 start destination을 설정했다면, 이제 destination간의 action을 설정할 차례다. action이란, destination간의 논리적 연결을 말한다. 추상적으로 보이는 말인데, 화살표로 표시되며 destination간의 전환을 의미한다. golbal action도 존재하는데, 이는 setting 화면처럼 언제 어디서나 접근 가능한 destination의 설정에 사용된다.
destination을 선택하면 다음 그림과 같이 동그란 노드가 표시된다.

동그라미로 표시되는 노드를 마우스로 드래그해서 destination간을 연결하면 action이 생성된다.

이렇게 action을 생성하고 xml로 보면, 다음과 같이 action이 기술되이 있다.
...
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="@string/label_blank"
tools:layout="@layout/fragment_blank" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/blankFragment2" />
</fragment>
...
소스가 되는 destination fragment에 <action>태그가 추가된 것을 볼 수 있다. <action>에는 고유 id와 이 화살표가 어느 destination을 향하고 있는지 기술되어 있다.
Destination간의 이동 구현 : NavController
위에서와 같이 navigation graph를 작성했다면, 이제 코드에서 버튼이 눌리는등의 이벤트시에 실제 이동을 구현해야 한다. 이는 NavHost안에서 navigation을 관리하는 NavController를 통해 가능하다. 코드내에서 NavController는 다음과 같이 얻을 수 있다.
- Fragment.findNavController()
- View.findNavController()
- Activity.findNavCOntroller(ViewId: Int)
예를들어 fragment에서 다음과 같이 사용 가능하다.
...
binding.buttonFirst.setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
...
NavController.navigate()를 이용하고 인자로는 해당 action의 id를 넣어주고 있다.
Global Action
Setting과 같이 어디서나 접근가능한 destination은 global action을 사용해 설정한다. 먼저, 그래프에서 destination을 추가한 후, 해당 destination을 선택하여 우클릭>Action>Global을 선택하면 된다. 그러면, destination왼쪽에 작은 화살표가 생긴다.

xml을 살펴보면, 다음과 같이 global action 태그가 추가된걸 확인할 수 있다.
...
<action android:id="@+id/action_global_mainFragment"
app:destination="@id/mainFragment"/>
...
이 후 사용법은 NavController를 통해 추가한 action을 이용하여 앞에서 본 방법과 동일하게 사용 가능하다.
Deep link 및 기타 심화내용은…
이렇게 기본적인 Navigation 구현과 사용법을 살펴봤다. 핵심만 살펴본다면, NavHost, Navigation graph, NavController의 세가지 일 것이다. 분명 알던 내용인데, 오랫만에 보려니 기억이 안나서 간단하게 정리를 했다. Editor의 보다 심화된 사용법이나 Deep link의 이용등은 공식 가이드 문서의 App navigation 파트를 살펴보기 바란다. 아마도, Compose에서의 사용이 제일 궁금한 부분일텐데, 그건 다음에 정리를 ㅋㅋ