앱을 탐색(Navigate)한다는 것은 다른 화면으로 이동한다는 것을 뜻한다. 화면 이동간의 발생하는 동작에서 activity와 Fragment간 전환(transition)이 늘어나거나 애니메이션이 추가되면 코드가 복잡해지며 점점 스파게티 코드가 된다.
이 Android Jetpack Navigation은 화면 이동 뿐만 아니라 앱 바, 탐색 창 등 여러 복잡한 기능을 쉽게 구현할 수 있도록 도와준다.
Google I/O 2018에서 SAA(Single Activity Architecture)를 사용하는 것을 권장한다는 내용이 있다.
이는 “하나 혹은 적은 개수의 Activity만을 사용하고 나머지 화면은 Fragment로 구성한 구조로, 주로 Jetpack Navigation과 함께 사용되는 구조이다” SAA 구조로 구현하기 위해서는 Jetpack Navigation을 활용하면 쉽게 구현할 수 있다.
❓ SAA(Single Activity Architecture)을 왜 적용해야 하는 것일까?
- Activity는 Fragment에 비해 상대적으로 무겁기 때문에 메모리, 속도 방면에서 Fragment를 사용하는 것이 이득이다.
- 비즈니스 로직을 Fragment 단위로 분리하여 의존성을 줄일 수 있다.
- Activity 보다 유연한 UI 디자인을 지원한다.
위 구성 요소들을 사용하여 화면 이동, 데이터 전달 등 Navigation의 기능들을 구현해보겠다.
Navigation은 Android Studio 3.3 이상에서 사용이 가능하다.
plugin {
...
id 'androidx.navigation.safeargs.kotlin'
}
dependencies {
def nav_version = "2.5.1"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
buildscript {
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1"
}
}
plugins {
...
}
navigation
을 사용할 때 activity가 아닌 fragment를 생성해서 서로를 연결해준다.
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
}
class SecondFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_second, container, false)
}
}
class ThirdFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_third, container, false)
}
}
총 세 개의 프래그먼트를 생성했다.
모든 navigation 정보가 모여있는 xml리소스인 NavGraph 파일을 생성해준다. 모든 fragment간의 플로우를 한 눈에 확인할 수 있다.
<?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>
/res/navigation/nav_graph.xml
경로에 생성해준다.
fragment들은 activity 위에서 동작해야한다. 그러므로 fragment들을 담아낼 화면을 어떤 화면으로 할 것인지 설정해주는 과정이다. MainActivity를 NavHost로 설정하겠다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<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"/>
</layout>
layout
안에 FragmentContainerView
뷰를 감싸주었다.
import androidx.databinding.DataBindingUtil.setContentView
import com.jaemin.androidplayground.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
}
만약 databinding을 사용하면 MainActivity의 setContentView를 DataBindingUtil의 setContentView로 수정한다.
<?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/first_fragment">
<fragment
android:id="@+id/first_fragment"
android:name="com.jaemin.androidplayground.FirstFragment"
android:label="First Fragment"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_first_fragment_to_second_fragment"
app:destination="@id/second_fragment"/>
</fragment>
<fragment
android:id="@+id/second_fragment"
android:name="com.jaemin.androidplayground.SecondFragment"
android:label="Second Fragment"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_second_fragment_to_third_fragment"
app:destination="@id/third_fragment"/>
</fragment>
<fragment
android:id="@+id/third_fragment"
android:name="com.jaemin.androidplayground.ThirdFragment"
android:label="Third Fragment"
tools:layout="@layout/fragment_third"/>
</navigation>
fragment 태그를 추가해주고 3개의 프래그먼트를 연결한다. action 태그를 사용하여 어느 프래그먼트로 이동할지 목적지 화면을 지정해준다.
app:startDestination
: 첫 화면으로 보여질 화면의 id를 정의한다.android:name
: 해당 프래그먼트 파일명을 넣어준다.tools:layout
: 레이아웃 미리보기에 보여질 layout을 편의상 넣어준다.💡 NavHostFragment는 개별적으로 NavController를 가지고 있다.
action 태그로 목적지 화면을 지정해주었으면 버튼을 누르면 해당 화면으로 이동하도록 구현을 해야한다. 이때 NavController 객체를 이용하여 NavHost에 보여지는 View를 변경하도록 할 수 있다.
그 전에 layout의 button id 값을 사용하기 위해 dataBinding를 사용했다.
buildFeatures {
dataBinding true
}
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context=".FirstFragment">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_first_fragment" />
<Button
android:id="@+id/btn_move"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="move"/>
</FrameLayout>
</layout>
layout 태그로 감싸주었고 button 뷰를 추가했다.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentFirstBinding.inflate(inflater, container, false)
context ?: return binding.root
binding.btnMove.setOnClickListener {
val direction =
FirstFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(direction)
}
return binding.root
}
fragment에서 button에 대한 setOnClickListener를 정의하고 findNavController 객체의 navigate 방향을 action 태그에서 지정했던 목적지로 설정해준다.
버튼 클릭 시 화면이 이동하는 것을 확인할 수 있다.
Navigation component를 이용하여 Bottom Navigation을 구현해보자.
/res/menu/menu_bottom_nav.xml
파일을 생성한다.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/first_fragment"
android:icon="@drawable/ic_home"
android:title="First"/>
<item
android:id="@+id/second_fragment"
android:icon="@drawable/ic_home"
android:title="Second"/>
<item
android:id="@+id/third_fragment"
android:icon="@drawable/ic_home"
android:title="Third"/>
</menu>
이때 menu 파일에서 item의 id와 nav_graph에서 정의된 fragment의 id 값이 동일해야한다.
activity_main.xml
에서 FragmentContainerView와 BottomNavigationView를 같이 정의해준다. 여기서 FragmentContainerView는 BottomNavigation의 각 탭에 보여질 화면들이 노출된다.
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
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="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/menu_bottom_nav"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
BottomNavigationView의 app:menu
는 아까 정의한 menu_bottom_nav
파일로 지정한다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
binding.bottomNav.setupWithNavController(navController)
}
}
supportFragmentManager로 nav_host_fragment의 아이디를 가져오고 해당 FragmentContainerView의 navController 객체를 BottomNavigation의 setupWithNavController
함수의 인자로 넣어준다.
참고로 setContentView는 DataBindingUtil 패키지에 포함되어있다. import androidx.databinding.DataBindingUtil.setContentView
BottomNavigation과 fragment가 연동된 것을 확인할 수 있다.
navGraph에서 fragment의 label을 지정해줬다.
<fragment
android:id="@+id/first_fragment"
android:name="com.jaemin.androidplayground.FirstFragment"
android:label="First Fragment"
tools:layout="@layout/fragment_first" />
이 라벨을 앱바의 title로 띄우기 위해서는 AppBarConfiguration을 사용할 수 있다.
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
MainActivity에서 setupActionBarWithNavController
를 정의한다. activity에서 정의했기 때문에 navController 객체를 NavHostFragment 에서 가져온다. AppBarConfiguration은 navController 객체의 graph 변수를 인자로 넣어준다.
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
AppBarConfiguration을 사용하게 되면 기본적으로 첫화면(startDestination)을 제외하고 모두 뒤로가기 버튼이 생긴다. 이 뒤로가기 버튼을 활성화 시키기 위해서는 onSupportNavigateUp
함수를 오버라이드 한다.
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.first_fragment,
R.id.second_fragment,
R.id.third_fragment
))
setupActionBarWithNavController(navController, appBarConfiguration)
기본적으로 생기는 뒤로가기 버튼을 제거하기 위해서는 AppBarConfiguration의 인자로 navController.graph
가 아닌 뒤로가기 버튼을 제거 할 fragment들의 id를 set 형태로 인자에 넣어준다.
라벨에 따른 앱바의 타이틀이 잘 나타나는 것을 확인할 수 있다.
/res/anim/slide_out_left.xml
파일을 생성한다.
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%" android:toXDelta="-100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="200"/>
</set>
<fragment
android:id="@+id/first_fragment"
android:name="com.jaemin.androidplayground.FirstFragment"
android:label="First Fragment"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_first_fragment_to_second_fragment"
app:destination="@id/second_fragment"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"/>
</fragment>
app:exitAnim
은 화면이 사라질 때 왼쪽으로 부드럽게 사라지도록 구현했고,
app:popEnterAnim
은 화면에 다시 나타날 때 왼쪽에서 부드럽게 나타나도록 구현했다.
화면 이동 시 부드럽게 이동하는 애니메이션이 추가된 것을 확인할 수 있다.
Navigation
Android - Navigation을 사용하는 방법
[Android] Single Activity Architecture (SAA) + Navigation
Bottom Navigation 사용하기