[Android] Jetpack - Navigation component

유재민·2022년 8월 28일
0
post-custom-banner

🍀 Android Jetpack - Navigation component

앱을 탐색(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)을 왜 적용해야 하는 것일까?

  1. Activity는 Fragment에 비해 상대적으로 무겁기 때문에 메모리, 속도 방면에서 Fragment를 사용하는 것이 이득이다.
  2. 비즈니스 로직을 Fragment 단위로 분리하여 의존성을 줄일 수 있다.
  3. Activity 보다 유연한 UI 디자인을 지원한다.

👍🏻 Navigation 장점

  • Fragment간의 화면 이동 흐름(플로우)을 한 눈에 파악할 수 있다.
  • 화면 전환에 대한 표준화된 애니메이션 리소스를 사용할 수 있다.
  • 탐색 창, 하단 탐색 등 탐색 UI 패턴을 쉽게 구현할 수 있다.
  • 프래그먼트 트랜잭션을 처리할 수 있다.
  • 데이터 전달을 안정하게 할 수 있다.
  • 딥링크를 처리할 수 있다.
    • 딥링크: 특정 페이지에 도달 할 수 있는 링크

📌 구성 요소

  1. Navigation Graph
    • 새로운 리소스 타입 (navigation)
    • 탐색에 관련된 모든 정보를 포함하고 중심화하는 XML 파일 형태
    • Fragment간의 모든 플로우를 확인할 수 있다.
  2. NavHost
    • Navigation Graph에서 대상을 표시하는 빈 컨테이너
    • Fragment 대상을 표시하는 NavHostFragment를 포함한다.
  3. NavController
    • NavHost에서 App Navigation을 관리하는 객체
    • 화면 이동 시 NavHost에서 대상 콘텐츠의 전환을 조종하는 역할

위 구성 요소들을 사용하여 화면 이동, 데이터 전달 등 Navigation의 기능들을 구현해보겠다.

Navigation은 Android Studio 3.3 이상에서 사용이 가능하다.


1. dependency 추가

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 {
	...
}

developers


2. Fragment 생성

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)
    }
}

총 세 개의 프래그먼트를 생성했다.


3. NavGraph 생성

모든 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 경로에 생성해준다.


4. NavHost 정의

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 뷰를 감싸주었다.

  • defaultNavHost : NavHostFragment가 시스템 뒤로 버튼을 가로챈다. 즉, true 일 때 back key를 누르면 이전 화면으로 전환되고 false 일 때 back key를 누르면 앱이 종료된다.
  • navGraph : graph를 정의한 파일을 의미
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로 수정한다.


5. NavGraph 정의

<?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을 편의상 넣어준다.

6. NavController로 화면 이동

💡 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 태그에서 지정했던 목적지로 설정해준다.

버튼 클릭 시 화면이 이동하는 것을 확인할 수 있다.


🧭 Bottom Navigation 구현

Navigation component를 이용하여 Bottom Navigation을 구현해보자.

1. menu 파일 생성

/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 값이 동일해야한다.

2. BottomNavigationView 추가

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 파일로 지정한다.

3. FragmentContainerView와 BottomNavigationView 연결

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가 연동된 것을 확인할 수 있다.


🍒 AppBarConfiguration 구현

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을 사용할 수 있다.

AppBar 구현

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 형태로 인자에 넣어준다.

라벨에 따른 앱바의 타이틀이 잘 나타나는 것을 확인할 수 있다.


💨 화면 이동 시 애니메이션 효과 넣기

1. anim 파일 생성 생성

/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>

2. action에 추가

<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 사용하기

profile
유잼코딩
post-custom-banner

0개의 댓글