Overview

수많은 안드로이드 앱들은 사용자가 앱 내부에 존재하는 콘텐츠들을 손쉽게 찾아볼 수 있도록 화면과 화면 사이를 이동할 수 있게끔 개발됩니다. 브랜디 앱 또한 다양한 화면들이 존재하고, 사용자들에게 다양한 화면들을 제공하기 위해 현재도 개발이 진행되고 있습니다.

오늘 소개해 드릴 Navigation 컴포넌트는 UI전환 외에도 Fragment 트랜잭션 관리, 딥링크 구현 및 처리, 안전한 데이터 전달 등 다양한 기능을 제공하고 있어 해당 내용에 대해 공유해 드리려고 합니다.

1. Fragment Manager에 대해

Fragment Manager는 프래그먼트를 추가, 삭제, 교체하고 백스택에 추가하는 작업을 실행하는 클래스입니다. 이 과정에서 개발자는 Fragment Manager를 통해 트랜잭션 관리를 하게 되는데, commit과 commitAllowingStateLoss()의 차이와 supportFragmentManager와 childFragmentManager에 대한 경험이 있다면 별문제가 되지 않지만, 경험이 상대적으로 부족할 경우 메모리 누수 및 앱이 종료되는 불상사가 발생할 수 있습니다.

  • java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState with DialogFragment

Navigation Component를 사용하게 되면 개발자를 대신해 Fragment Manager를 사용하기 때문에 직접적으로 상호작용하지 않아도 됩니다. Fragment, Backstack, Fragment Manager 사용에 대한 권장 사항을 따르므로 개발자가 신경 써야 할 점들이 줄어듭니다.

2. UI 전환

모바일 어플리케이션에서 UI 전환은 꼭 필요한 기능입니다. Navigation Component를 사용하지 않고 Fragment를 사용한 UI 전환을 구현하려고 한다면 트랜잭션 관리와 다음 화면에 필요한 데이터, 애니메이션 처리 코드까지 작성해야 하는데 네비게이션 컴포넌트는 네비게이션 그래프 지원, Shared Element Animation 지원, Pop Stack 관리 등을 제공하기 때문에 쉽게 개발이 가능합니다.

  • 사용하기 전
val blankFragment = BlankFragment()
        blankFragment.arguments = Bundle().apply {
            putString("name", "test")
        }

childFragmentManager.beginTransaction()
        .setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right)
        .add(blankFragment, "blankFragment")
        .commitNowAllowingStateLoss()

  • 사용한 후
val bundle = Bundle().apply {
       putString("name", "test")
}

val navOptions = NavOptions.Builder()
       .setEnterAnim(R.anim.nav_default_enter_anim)
       .setExitAnim(R.anim.nav_default_exit_anim)
       .build()

findNavController().navigate(R.id.action_to_blank, bundle, navOptions)

FirstFragment에서 BlankFragment로 이동

3. NavigationUI

Navigation Component는 NavigationUI 클래스를 포함하고 있는데 이 클래스는 상단 앱바, Navigation View, BottomNavigationView 를 네비게이션 컴포넌트로 관리할 수 있도록 기능을 제공하고 있습니다.

네비게이션 컴포넌트를 사용하지 않을 경우, 홈 버튼 표시 여부, 홈 버튼 아이콘 변경, drawer이 열렸을 때, 닫혔을 때의 변경사항, 홈 버튼이 클릭 되었을 경우 등등 신경 써주어야 할 점들이 굉장히 많은데, NavigationUI를 사용하면 매우 간편하게 설정할 수 있습니다.

06
07

아래 코드는 Navigation Component로 Toolbar와 NavigationView를 사용하기 전, 후 비교 코드입니다.

  • 사용하기 전
override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setSupportActionBar(binding.toolbar)
        supportActionBar?.let { actionBar ->
            actionBar.setDisplayHomeAsUpEnabled(true)
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu)
        }

   binding.layoutDrawer.addDrawerListener(object : DrawerLayout.DrawerListener {
            override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
            }

            override fun onDrawerOpened(drawerView: View) {
            }

            override fun onDrawerClosed(drawerView: View) {
            }

            override fun onDrawerStateChanged(newState: Int) {
            }
        })
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> {
                binding.layoutDrawer.openDrawer(GravityCompat.START)
            }
        }
        return super.onOptionsItemSelected(item)
    }
  • 사용한 후
val navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(navController.graph, binding.layoutDrawer)

binding.toolbar.setupWithNavController(navController, appBarConfiguration)
binding.navigationView.setupWithNavController(navController)

4. Safe Arguments

Bundle을 이용해 Fragment 간의 데이터를 전달할 때, 개발자가 직접 데이터의 key-value를 지정하게 되는데 데이터를 가져오는 과정에서 데이터의 이름이나 타입이 맞지 않을 경우 에러가 발생하거나 예상과 다른 상황이 생길 수 있습니다. 네비게이션 컴포넌트는 SafeArgs라는 Gradle Plugin을 지원하며, 타입 안전성을 보장합니다.

  • 사용하기 전
11
11
보내는 타입 : Int, 받는 타입 : String 실행 시 공백 출력
  • 사용한 후
11
11
실행 결과 : 99


SafeArgs를 사용하게 되면 데이터를 보내는 클래스 이름 뒤에 “Directions”가 붙은 클래스가 자동으로 생성되고, 데이터를 전달하는 action 같은 경우도(R.id.action_to_resultFragment) ActionToResultFragment라는 이름의 내부 클래스가 생성됩니다.

만약 default value을 설정하지 않은 상태에서 공백 또는 다른 타입의 데이터를 보내려고 한다면 아래와 같이 컴파일 에러가 발생하기 때문에 개발자가 더욱 안전하게 개발할 수 있습니다.

11
11

5. Nested Graph

앱을 개발하다 보면 화면이 굉장히 많아지게 되고, 하나의 NavGraph가 복잡해지면 관리하기도 힘들어집니다. 네비게이션 컴포넌트는 Nested Graph(중첩 그래프)를 지원하며, 개별 NavGraph를 만들어 상위 NavGraph에 추가할 수 있습니다. 아래 그림은 상위 NavGraph에 태그를 이용해 NavGraph를 추가한 예제입니다.

11
11

만약 여러 NavGraph를 만들어 사용하는 프로젝트라면, 어떤 화면에서든, 특정 화면이 랜딩되어야 하는 경우가 있습니다. 브랜디를 예를 들자면, 상품 상세 페이지가 있습니다. 만약 navigation 태그를 사용할 경우 상품 상세를 추가하는 코드가 중복되겠지만 include 태그를 사용할 경우 중복 없이 여러 NavGraph 내의 상품 상세 NavGraph를 포함하여 사용할 수 있습니다.

11

같은 그래프 내 ViewModel 공유(NavGraphViewModel)

같은 그래프 내 뷰 모델 공유는 Nested Graph를 기준으로 ViewModel을 생성하고, 그래프에 속한 Fragment끼리 UI관련 데이터를 공유할 수 있습니다. 해당 기능은 화면이 많은 로그인, 회원가입, 체크아웃 화면 등에 유용하게 사용할 수 있습니다.

val factory: ViewModelProviderFactory = ...
val viewModel: MyViewModel
    by navGraphViewModels(R.id.my_graph) { factory }


Conclusion

개인적으로 선호하는 안드로이드 라이브러리 중 하나이지만, 네비게이션 컴포넌트 이외에도 많은 라이브러리로 인해 개발이 편해지고 있습니다. 글을 작성하면서 느꼈던 점은 프로덕트를 구성하는 라이브러리에 대한 공부도 중요하지만, 기초에 대한 공부 또한 열심히 해야 프로덕트에 버그가 발생 했을 때 당황하지 않고 대응할 수 있겠다는 생각이 들었습니다. 이 글이 안드로이드 개발에 조금이라도 도움이 되시길 바라면서 글을 마칩니다. :)

참고자료

https://developer.android.com/guide/navigation/navigation-getting-started?hl=ko


이다원 | APPS실 MA팀
브랜디, 오직 예쁜 옷만