사용자는 앱을 다양한 콘텐츠를 접하기 위해 여러 화면을 왔다갔다 이동한다. 이때 Jetpack에서 제공하는 Navigation을 활용하면 단순하게 화면(Fragment) 전환을 구현할 수 있다. 뿐만 아니라 화면 간의 데이터 공유도 훨씬 쉽다!
공식 문서는 다음과 같이 말한다.
그러니까 Fragment 전환 시 아주 간단하고 데이터 공유도 쉽고 안전하니까 쓰라는 거 같다~
아래 그림처럼 화면들이 어떻게 연결되어 있는지 시각적으로 보여주는 XML 리소스 파일. 그러니까 사용자가 앱에서 이동할 수 있는 경로를 그릴 수 있다.
navigation graph 내의 Fragment가 표시되는 빈 컨테이너.
navHost를 움직여서 화면을 보여줄 수 있게 하는 실질적인 존재. 리모컨처럼 보면 되겠다. 사용자는 NavController에게 Navigation Graph의 경로를 따라 움직이거나 특정 화면으로 가고 싶다고 말하고, 그럼 NavController는 사용자가 원한 화면, 그러니까 목적을 NavHost에 띄운다.
사실 둘이 별 차이는 없는데 아카이빙 목적으로 넣어둔다.
공식문서(22.08.31) ver.
// 프로젝트 build.gradle
buildscript {
repositories {
google()
}
dependencies {
val nav_version = "2.5.1"
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version")
}
}
// 앱 build.gradle
plugins {
id("androidx.navigation.safeargs.kotlin")
}
dependencies {
val nav_version = "2.5.1"
// Java language implementation
implementation("androidx.navigation:navigation-fragment:$nav_version")
implementation("androidx.navigation:navigation-ui:$nav_version")
// Kotlin
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
// Feature module Support
implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")
// Testing Navigation
androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
// Jetpack Compose Integration
implementation("androidx.navigation:navigation-compose:$nav_version")
}
goggle developer codelab ver.
// 프로젝트 build.gradle
buildscript {
ext {
nav_version = "2.3.1"
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
// 모듈 build.gradle
plugins {
id 'org.jetbrains.kotlin.android'
id 'androidx.navigation.safeargs.kotlin'
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
plugins를 사용하는 버전
// 프로젝트 build.gradle
plugins {
id 'com.android.application' version '7.2.2' apply false
id 'com.android.library' version '7.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id 'androidx.navigation.safeargs.kotlin' version '2.5.1' apply false
}
// 모듈 build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id("kotlin-parcelize")
id("androidx.navigation.safeargs.kotlin")
}
dependencies {
// navigation
def nav_version = "2.5.1"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
}
리소스 파일을 만들자.
New Resource File을 활용해 만들고, 그러면 아래와 같은 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"
android:id="@+id/nav_graph">
</navigation>
처음엔 당연히 디자인 탭에도 아무것도 없다.
Fragment를 추가하고 연결하자.
위와 같이 추가를 하고,
드래그를 해서 Fragment와 Fragment 간 연결하자.
연결되면 위와 같이 확인할 수 있다.
참고로 Fragment 이름 옆 집 아이콘은 시작점을 의미한다. Fragment를 클릭하고 위 아이콘을 눌러서 지정할 수 있다.
여기까지 하고 나면 아래처럼 code가 바뀐 걸 알 수 있다!
<?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/letterListFragment">
<fragment
android:id="@+id/letterListFragment"
android:name="com.example.wordsapp.LetterListFragment"
android:label="@string/app_name"
tools:layout="@layout/fragment_letter_list">
<action
android:id="@+id/action_letterListFragment_to_wordListFragment2"
app:destination="@id/wordListFragment" />
</fragment>
<fragment
android:id="@+id/wordListFragment"
android:name="com.example.wordsapp.WordListFragment"
android:label="@string/word_list_fragment_label"
tools:layout="@layout/fragment_word_list">
<argument
android:name="letter"
app:argType="string" />
<action
android:id="@+id/action_wordListFragment_to_letterListFragment"
app:destination="@id/letterListFragment" />
</fragment>
</navigation>
XML에 다음과 같이 NavHostFragment
를 추가해준다.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
</FrameLayout>
android:name
→ 구현 클래스의 이름app:defaultNavHost="true"
→ 시스템이 뒤로가려고 할 때 NavHostFragment가 인터셉트한다. NavHost에 딱 하나가 기본값으로 지정되어야 한다.app:navGraph
→ NavHostFragment와 Navigation Graph를 연결한다.다음 코드는 위에서 만든 navHost를 id값 nav_host_fragment로 참조해 가져와 navController에 할당한다.
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
그리고 아래 코드를 이용하면 Fragment의 전환에 따라 액션바도 Fragment를 따라서 함께 움직이도록 세팅할 수 있다.
setupActionBarWithNavController(navController)
이름 그대로 액션바를 세팅하는 건데…무슨 말인지 이해가 어렵다면 캡쳐로 한 눈에 보자!
아래 onSupportNavigateUp()을 활용하면 액션 바에서 뒤로가기 버튼을 누를 수 있게 된다.
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
전체 Acticity 코드는 아래와 같다.
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
Safe Args는 NavDirections 객체를 반환하는 메서드로, SpecifyAmountFragmentDirecrions 클래스를 생성한다. 참고로 이 클래스의 이름은 자동으로 생성된다.
override fun onClick(view: View) {
val action =
SpecifyAmountFragmentDirections
.actionSpecifyAmountFragmentToConfirmationFragment()
view.findNavController().navigate(action)
}
이게 뭔 소리냐면, NavGraph의 Fragment를 따라 알아서 생성해준다는 말이다. 예를 들어 NavGraph에 LetterListFragment로 시작되는데, 그 Fragment는 아래와 같은 식으로!
// LetterListFragment 버튼의 text를 WordListFragment로 전달하는 예제
holder.button.setOnClickListener {
val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())
holder.view.findNavController().navigate(action)
}
좀 더 정리해보면 이런 식으로 알아서 생성된다는 거다.
val action = {출발Fragment}Directions.action{출발Fragment}To{목적Fragment}()
추가 포스트에서 좀 더 자세하게 SafeArgs를 알아보자
navGraph의 속성에서 label을 활용하면 액션바의 내용을 바꿀 수 있다. 이것과 Safe Args를 활용하면 데이터에 따라 유동적으로 변환도 가능한데 아직 이해가 제대로 되지 않아 나중에 추가 포스팅을 하겠다.