Navigation은 하나의 Activity 안에서 여러 Fragment간의 전환에 중점을 두고 설계가 되었습니다.
이러한 Fragment의 전환에 있어 보일러플레이트를 제거하고, 데이터 전달과 스택 관리에있어 이점이 있습니다.
공식 문서에서는 다음과 같이 설명하였습니다.
또한 아래와 같이 Navigation Editor에서 미리보기를 지원합니다.
dependencies 설정
아래와 같이 dependencies를 설정합니다.
build.gradle(module)
dependencies{
...
implementation 'androidx.navigation:navigation-fragment:2.5.0'
...
}
프래그먼트 생성
아래 사진과 같이 New -> Fragment 를 통해서 생성할 수 있습니다.
저는 아래와 같이 프래그먼트와 해당 레이아웃 파일을 만들었습니다.
OneFragment.kt
package com.example.jetpacksample
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.jetpacksample.databinding.FragmentOneBinding
class OneFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentOneBinding.inflate(layoutInflater)
return binding.root
}
}
fragment_one.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
tools:context=".OneFragment">
<TextView
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="it is one Activity\n" />
<Button
android:id="@+id/btnOneByOne"
android:text="go to One Activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btnTwoByOne"
android:text="go to Two Activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btnThreeByOne"
android:text="go to Three Activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.appcompat.widget.LinearLayoutCompat>
네비게이션 그래프 생성
아래와 같이 res폴더에서 new -> resource file를 통해 탐색 그래프를 생성합니다
에디터의 그림과 같은 표시를 클릭하면 프래그먼트를 생성하고 화살표로 연결지어 액션을 생성합니다.
스택 관리와 애니메이션을 설정할 수 있습니다.
nav_graph.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/menu_one">
<fragment
android:id="@+id/menu_one"
android:name="com.example.jetpacksample.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" >
<action
android:id="@+id/action_oneFragment_to_twoFragment"
app:destination="@id/menu_two" />
<action
android:id="@+id/action_oneFragment_to_threeFragment"
app:destination="@id/menu_three" />
</fragment>
<fragment
android:id="@+id/menu_two"
android:name="com.example.jetpacksample.TwoFragment"
android:label="fragment_two"
tools:layout="@layout/fragment_two" >
<action
android:id="@+id/action_twoFragment_to_oneFragment"
app:destination="@id/menu_one" />
<action
android:id="@+id/action_twoFragment_to_threeFragment"
app:destination="@id/menu_three" />
</fragment>
<fragment
android:id="@+id/menu_three"
android:name="com.example.jetpacksample.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three" >
<action
android:id="@+id/action_threeFragment_to_oneFragment"
app:destination="@id/menu_one" />
<action
android:id="@+id/action_threeFragment_to_twoFragment"
app:destination="@id/menu_two" />
</fragment>
</navigation>
여기서 각 fragment의 id 값은 menu에서 설정한 id값과 동일해야 작동한다.
프래그먼트 화면 설정
생성한 탐색 그래프를 띄워줄 프래그먼트 화면을 메인 레이아웃에 설정합니다.
activity_main.xml
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
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"
app:defaultNavHost="true"/>
defaultNavHost : NavHostFragment가 시스템 뒤로 버튼을 가로챕니다. 하나의 NavHost만 기본값으로 지정할 수 있습니다. 동일한 레이아웃에 여러 호스트가 있다면(예: 창이 2개인 레이아웃) 한 호스트만 기본 NavHost로 지정해야 합니다.
이후에 나올 바텀네비게이션과 서랍UI의 경우 뒤로가기시 NavHost로 이동할지 여부입니다
navGraph : 생성한 탐색그래프 파일
name : navHostFragment임을 명시
앞서 탐색 그래프에서 생성했던 액션을 인자로 프래그먼트를 전환합니다.
다음과 같이 작성했던 OneFragment에 클릭리스너를 추가해서
navController에 navigate를 이용해서 이동할 수 있습니다.
binding.btnTwoByOne.setOnClickListener{
findNavController().navigate(R.id.action_oneFragment_to_twoFragment)
}
네비게이션으로 만든 프래그먼트 및 그래프를 하단에 메뉴로 표기할 수 있습니다.
dependencies 설정
네비게이션을 이용한 UI를 만드려면 다음과 같은 의존성을 주입합니다.
build.gradle(module)
dependencies{
...
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
...
}
먼저 하단에 들어갈 아이콘과 이름을 설정할 메뉴 파일을 생성합니다.
res -> new -> Menu Type 설정으로 진행할 수 있습니다.
res/menu/nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_one"
android:icon="@drawable/ic_baseline_music_note_24"
android:title="one" />
<item
android:id="@+id/menu_two"
android:icon="@drawable/ic_baseline_place_24"
android:title="two" />
<item
android:id="@+id/menu_three"
android:icon="@drawable/ic_baseline_newspaper_24"
android:title="three" />
</menu>
그리고 메인 레이아웃에 바텀 네비게이션을 추가하고 메뉴를 설정합니다
activity_main.xml
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/nav_menu" />
아래와 같이 host fragment를 지정해서 네비게이션 탐색 그래프가 활동할 위치를 지정해줍니다.
activity_main.xml
<fragment
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navBottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
바텀 네비게이션 컨트롤러 설정
아래와 같이 컨트롤러를 설정해줌으로써 작동합니다.
바텀 네비게이션은 이동할 때 스택이 쌓이지 않으며 xml에서 defaultNavHost속성을 true 했을 때 host fragment로 돌아갑니다.
MainActivity.kt
val navController = findNavController(R.id.fragmentContainerView)
binding.navBottom.setupWithNavController(navController)
host fragment를 <Fragment ... /> 가 아닌 androidx.fragment.app.FragmentContainerView등 다른 레이아웃을 이용했을 경우
navController를(supportFragmentManager.findFragmentById(R.id.호스트Fragment) as NavHostFragment).navController
로 지정해주어야 navController를찾을수 있습니다.
DrawerLayout은 drawer(서랍)이라는 이름에서 알 수 있듯이
평소에는 화면의 한쪽에 숨겨져 있다가 사용자가 액션을 취하면 화면에 나타날 수 있도록 하는 레이아웃입니다.
레이아웃을 위해서 먼저 res/values/themes.xml에서 액션바를 지우고 커스텀 툴바를 넣는다.
보통 서랍 UI를 여는 버튼은 액션바에 존재하기에 해당 버튼의 배치 및 아래 사진과 같이 서랍이 액션바 밑으로 가게되어 시인성이 떨어진다.
기본 액션바 지우기
res/values/themes.xml
<style name="Theme.JetpackSample" parent="Theme.MaterialComponents.DayNight.NoActionBar">
새로운 툴바 생성
activity_main.xml
...
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/teal_700"/>
...
Header layout 설정
위에서 표시한 부분인 drawer의 header 부분을 보여줄 레이아웃을 생성한다. 해당 레이아웃은 이후 서랍UI에 속성으로 적용된다
layout/nav_drawer_header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
android:background="@color/teal_200"
android:gravity="bottom"
android:orientation="vertical"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
app:srcCompat="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="Android Studio"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:text="android.studio@android.com" />
</LinearLayout>
Drawer Navigation을 레이아웃에 설정하려면 최상위 레이아웃을 DrawerLayout으로 설정하고 Drawer navigation이 기존 루트 레이아웃까지 덮을 수 있도록 맨 아래에 추가한다.
activity_main.xml
<androidx.drawerlayout.widget.DrawerLayout
...>
<!--기존 루트 레이아웃-->
<androidx.constraintlayout.widget.ConstraintLayout
...>
...
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navDrawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_drawer_header"
app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
headerLayout 속성에 이전에 만들었던 header layout파일을 설정한다
menu 속성은 header 밑에 나올 컨텐츠 목록으로 기존에 쓰던 메뉴파일과 동일하게 설정
layout_gravity속성으로 왼쪽 또는 오른쪽에서 나오도록 지정
이제 서랍UI를 나오게 할 메뉴버튼을 툴바에 적용하자
메인 액티비티에서 다음의 코드로 버튼을 생성한다.
setSupportActionBar(binding.toolbar) // 액션바 등록
supportActionBar?.setDisplayHomeAsUpEnabled(true) // 왼쪽 상단 버튼 만들기
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_menu_24) //왼쪽 상단 버튼 아이콘 지정
이제 navController에 drawerNavigation을 설정해서 동기화 해보도록 한다
binding.navDrawer.setupWithNavController(navController)
이제 메뉴 버튼을 누를때마다 서랍UI가 열릴 수 있도록 툴바에 리스너를 onOptionsItemSelected 메서드를 오버라이드해서 등록한다
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
android.R.id.home -> binding.mainActivityLayout.openDrawer(Gravity.START)
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if(binding.mainActivityLayout.isDrawerOpen(binding.navDrawer))
binding.mainActivityLayout.closeDrawer(Gravity.START)
else
super.onBackPressed()
}
샘플 코드
https://github.com/WorldOneTop/AndroidJetpackSample/tree/Navigation
참고 사이트
https://developer.android.com/guide/navigation
https://medium.com/nbt-tech/android-navigation-%EC%93%B8%EA%B9%8C%EB%A7%90%EA%B9%8C-964388a562e0
https://material.io/components/navigation-drawer