남잭슨의 개발 블로그

[Android Jetpack] AAC Navigation Component - 1. Navigation 소개,구성 및 개요 , BackStack관리 본문

Android

[Android Jetpack] AAC Navigation Component - 1. Navigation 소개,구성 및 개요 , BackStack관리

남잭슨 2019. 3. 31. 18:20

Navigation
Android Jetpack 의  AAC (Android Architecture Component) 컴포넌트중 Navigation에 대해 알아보자! 

네비게이션은 앱내의 화면 이동 구현을 도와주는 AAC 라이브러리이다. 

AAC 라이브러리는 기존에 없었던 기능이 아니라, 개발자들이 구현하기 쉽게 도와주는 컴포넌트라고 생각하면된다.

 

아래의 화면처럼 Android 내에서 화면흐름을 시각적으로 볼수있을 뿐만아니라 바로 수정할 수 있기때문에 편하다! 

 

이 뿐만아니라 아래처럼 여러 기능들을 제공해준다.

 

Navigation은 앱내에서 화면 탐색에 필요한 모든 것을 처리할 수 있다.

 

  • Fragment 트랜잭션을 관리할수 있다. 
  • Up, Back 버튼의 작업 등(백스택 관리)을 간단하게 처리
  • 화면 전환시, Animation 이나 Transition을 위한 표준화된 리소스를 제공
  • 딥링크 구현 및 처리
  • Navigation UI 패턴을 사용한 Navigation drawers, Bottom Navigation의 연동을 쉽게 구현 가능하게 지원
  • fragment간의 이동시 안전하게 데이터를 전달 가능
  • Navigation Editor를 통해 화면흐름을 시각적으로 관리할 수 있다.

기본 FragmentManager, Intent , Bundle등의 사용을 대체할수 있다. 

 

1. Navigation의 구성요소

Navigation의 구성요소는 3가지로 나눠서 이해하면된다. 

  • Navigation Graph 
  • Nav Host
  • Nav Controller

1. Navigation Graph 

Navigation Graph 는 res/navigation/ 폴더에 추가한 xml 파일이다.

화면이동에 대한 모든정보( Action, 화면이동시 파라미터, 화면단위)를 정의하는 곳이다. 

위와 같은 화면 흐름을 시각적으로 보고 관리할 수 있는 파일이며, Android Studio 3.3 이상에서 지원한다.

NavGraph에는 아래의 기본 요소에 대한 태그들을 사용할 수 있다.

  • <navigation>  - NavGraph의 기본 태그 
  • <fragment> or <Activity> or <Dialog>등의 Destination ( 목적지가 되는 하나의 화면라고 생각하면된다. ) ( 그래프에서는 하나의 화면으로 구분된다.) ( <Dialog>태그는 2.1.0부터 지원한다. ) 
  • <action> -  화면 이동에 대한 액션을 정의할 수 있다. ( 화면간의 이동을 정의하는 태그 )  ( 그래프에서는 화살표로 표현된다.)
  • <argument> - 화면 이동에 대한 파라미터를 정의한다.
  • <deeplink> - 딥링크에 대한 내용을 정의한다.

위의 태그를 이용하여, 아래와 같이 각 화면들과 화면 이동에 대한 내용이 정의된 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"
            app:startDestination="@id/dashboard"
            android:id="@+id/mobile_navigation">


    <fragment
            android:id="@+id/home_list"
            android:name="com.namjackson.arch.sample.view.ui.home.HomeListFragment"
            android:label="@string/fragment_home_list"
            tools:layout="@layout/home_list_fragment">

        <action
                android:id="@+id/action_to_detail"
                app:destination="@id/detailFragment"/>
        <action
                android:id="@+id/action_to_add"
                app:destination="@id/addFragment"
                app:enterAnim="@anim/slide_in_up"
                app:exitAnim="@anim/fade_out"
                app:popEnterAnim="@anim/fade_in"
                app:popExitAnim="@anim/slide_out_down"/>
    </fragment>

    <fragment android:id="@+id/detailFragment"
              android:name="com.namjackson.arch.sample.view.ui.detail.DetailFragment"
              android:label="detail_fragment"
              tools:layout="@layout/detail_fragment">
        <action
                android:id="@+id/action_to_progress"
                app:destination="@id/progressFragment"/>

        <argument
                android:name="itemId"
                app:argType="integer"
                android:defaultValue="0"/>
    </fragment>

    <fragment android:id="@+id/progressFragment"
              android:name="com.namjackson.arch.sample.view.ui.detail.ProgressFragment"
              android:label="progress_fragment"
              tools:layout="@layout/progress_fragment">
        <action
                android:id="@+id/action_to_detail"
                app:destination="@id/detailFragment"/>
        <argument
                android:name="itemId"
                app:argType="integer"
                android:defaultValue="0"/>
    </fragment>

    <fragment android:id="@+id/errorFragment"
              android:name="com.namjackson.arch.sample.view.ui.ErrorFragment"
              android:label="ErrorFragment"
              tools:layout="@layout/error_fragment"/>

    <action
        android:id="@+id/action_to_error"
        app:destination="@id/errorFragment"/>

</navigation>

각각의 Fragment,Activitiy는 하나의 화면인 Destination 을 의미하고,

Fragment내에 action,argument 태그는 각각 화면을 이동 , 파라미터를 의미한다.

 

 

2.Nav Host

NavHost는 NavGraph에 정의된 화면들을 보여주는 컨테이너의 역할을 한다.

화면이동에 대한 액션은 모두 NavHost라는 Fragment에게 위임된다.

일반 Fragment의 Name에 NavHostFragment를 정의해주어야하고, 

연결되는 navGraph 또한 정의해주어야한다.

        <fragment
                android:id="@+id/nav_container"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:name="androidx.navigation.fragment.NavHostFragment"
                app:defaultNavHost="true"
                app:navGraph="@navigation/mobile_navigation"/>

NavHost에는 NavController도 포함한다.

 

3.NavController

NavController는 화면이동에 대한 컨트롤러 역할을 한다.

NavController는 NavHost에서 얻을수있다.

navController는 아래 처럼 여러 방법으로 가져온다.

 <!--navController fragment,activity,view에서 find -->
 val navController = findNavController()
 
 <!--navController navhost id로 가져오기 -->
 val navController = findNavController(R.id.nav_container)
 
 <!--navController - navhost에서 가져오기 -->
 val host: NavHostFragment = supportFragmentManager
                .findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment? ?: return
 val navController = host.navController

 

아래와 같이 navController를 사용하여 간단하게 화면 전환이 가능하다.

아래의 간단예제외에 많은 사용방법이있다. 

<!--NavGraph(xml)에 정의된 R.id.flow_step_one_dest에 해당하는 action이 진행된다.-->
findNavController().navigate(R.id.flow_step_one_dest)

<!--
  NavGraph에 정의된 Action(R.id.next_action)을 사용한다 
  HomeFragmentDirections은 intent개념으로 이해하면된다.
  NavGraph(xml)에 action을 정의하면 tool내에서 자동으로 해당 클래스를 생성해준다.
-->
findNavController().navigate(HomeFragmentDirections.nextAction(1))

<!--NavGraph에 정의된 Action(R.id.next_action)으로 Listner를 반환한다. -->
Navigation.createNavigateOnClickListener(R.id.next_action, null)

 

Navigation의  Action Structure

NavDestination,NavDirections,NavArgs은 NavGraph(xml)에서

각각 정의된 Destination, Action, Argument를 기반으로 자동 생성된 클래스이다.

아래처럼 Nav 클래스는 오른쪽에 매핑된다고 보면된다.

  • NavHost / NavController  = Activity / FragmentManager
  • NavGraph                         = res/navitaion(xml)
  • Navigator
  • NavDestination                = Fragment  
  • NavDirections / NavArgs  =  Intent / Bundle
  • NavigationUI                    = UI Helper

 

화면이 이동될때, NavController가 다음 화면으로 향하는 Action을 동작시켜주는 NavDiretions을 요청하면, NavBackStackEntry에서 Stack을 처리한후, 다음 NavDestination을 NavArgs과 함께 실행한다. 

사실 사용할땐 Navigation과 Destination,NavArgs 만 알면 사용할수있다.

 

Navigation BackStack 관리

 

Backstack에 관한 네이게이션의 기본원칙들을 먼저 알아보자

1. 앱은 반드시 고정된 시작점(Starting Destination)을 가진다.

( 고정된 시작점은 사용자가 보는 첫번째 화면이자, 사용자들이 Back버튼을 눌러서 도착하는 마지막 화면  )

(  로그인같은 일회성 화면이 시작화면이 되어서는 안된다. )

2. Navigation Stack은 화면의 상태를 나타낸다.

( 스택은 LIFO 구조로, 시작 Destination은 가장 아래에 있고 현재 Destination은 항상 위에 있다. 

만약 스택을 변경할땐, Stack의 최상단에서 push,pop으로 동작해야한다.)

3. Up버튼으로 앱을 종료시킬수는 없다.

( 시작 Destination에서는 Up버튼이 보이지않아야 한다.)

4. Up 과 Back 버튼은 동작이 일치해야한다. 

5. Deep Linking이나 Navigation이나 특정 Destination 진입 시, 동일한 Stack을 가져야한다. 

( DeepLink로 들어온 화면일지라도, Stack은 기존과 같이  고정된 시작점(Starting Destination)부터 쌓여있어야한다.)

 

위의 원칙을 기본으로 

Navigation에서는 이전 Destination(Fragment) 이 포함된 백스택을 자동으로 관리한다.

앱을 열때 첫화면이 백스택에 배치된다. 그 후, Navigate()를 실행시켜서 화면을 이동할때마다 

백스택 맨위에 해당 Destination(Fragment)이 놓여집니다. 

이전(뒤로가기, 위로가기) 기능 시 , 스택의 맨위의 Destination(Fragment)를 제거(or Pop)함으로

바로 이전의 (바로아래의 ) Destination(Fragment)가 호출된다.  (NavController.navigateUp() or NavController.popBackStack())

이런 작업들 Navigation에서 관리해준다.

popUpTo 및 popUpToInclusive

Navigation의 Action을 사용하여 이동할때, 일반 BackStack 외에도 추가목적지를 백스택에서 Pop 할수 있다. 

예를들어, 로그인 흐름등의 경우는 로그인 완료 이후, 사용자가 뒤로가기 이동시 다시 로그인 흐름으로 가져가지 않고, 

로그인 외의 흐름이동을 가져와야한다.

특정 Destination(Fragment)에서 다른 Destination(Fragment)으로 이동할때 ,

<Action> 태그안에 app:popUpTo를 추가하면 된다. 

app:popUpTo=Destination(Fragment)을 추가하면, 특정 Destination(Fragment)로 Pop하도록 Navigation에서 처리한다. 

또한 app:popUpToInclusive="true" 추가시, app:popUpTo에 지정된  Destination(Fragment)이 백스택에서도 제거된다. 

<action android:id="@+id/next_action"
        app:popUpTo="@id/login1"
        app:popUpToInclusive="true"
        app:destination="@id/home"/>

login2에서 위의 Action을 실행되면 

Home Destination(Fragment)으로 이동하고, 

app:popUpTo="@id/login1"

@id/login1까지의 백스택(login2)이 사라진다 

app:popUpToInclusive="true" 이기 때문에 

@id/login1의 백스택도 함께 Pop되어 

결과적으로 백스택에서 login1,login2가 같이 사라진다.

 

만약 해당옵션을 사용하지 않았다면, Home화면에서 Pop or Back 버튼시 Login2화면이 떳을것이다.

이렇게 BackStack을 간단한 옵션을 통해 관리할수 있다. 

 

Custom BackPressed 

BackButton에 대한 커스텀또한 지원해준다.

class MyFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // This callback will only be called when MyFragment is at least Started.
        val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
            // Handle the back button event
        }

        // The callback can be enabled or disabled here or in the lambda
    }
    ...
}

Back 버튼 관리를 위한 Custom BackPressed callback을 지원한다! 

 

샘플 코드

해당 예제 코드는 아래 깃허브에 구현하였습니다.

(네비게이션 외에도 클린아키텍쳐 스터디를 위해 오픈소스를 진행하고 있습니다. 많은 Star 와 많은 PR,Issue 참여해주세요! )

https://github.com/namjackson/Clean-Architecture-App

 

namjackson/Clean-Architecture-App

Sample app for studying the Android clean architecture. - namjackson/Clean-Architecture-App

github.com

 

 

다음글 

AAC Navigation Component -1. Navigation 소개, 구성 및 개요, BackStack 관리 ( 현재글 )

AAC Navigation Component -2. SafeArgs & Deeplink & Action ( 다음 글 )

AAC Navigation Component -3. NavigationUI & Animation/Transition ( 다음 글 )

AAC Navigation Component -4. Navigation Upgrade (ViewModel with NavGraph Scope & Modularizing(Nested NavGraph) & Multiple-Start-Destination ) & 나의 후기, 장단점( 다음 글 )

 

 

참고링크 

https://medium.com/@fornewid/navigation-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-82d23fbc85af

https://developer.android.com/guide/navigation/navigation-getting-started

Comments