Android 공부/Android UI

안드로이드 - Jetpack Navigation 사용 [코드 리팩토링]

0. 사용하는 이유

 '담다' 앱은 2년 전에 레이아웃의 구조나, 액티비티의 계층 구조에 대해서 전무할 때부터 만들고 관리한 앱이다.

메인화면은 바텀 네비게이션 구조에, 5개의 주제로 앱의 서비스를 제공하고 있다. 그런데, 기존의 구조는 main.xml에서 visible과 gone을 통해서 2000여줄의 레이아웃을 관리하는 구조였고, 최근 들어서 느린 렌더링으로 앱이 사용자에게 서비스를 제공한다는 것을 알았고, 구조를 바꾸기 시작했다.

1. 생각한 구조

 기존의 구조는 바텀 네비게이션을 클릭하면 코드에 의해서, 모든 레이아웃을 visible과 gone을 통해서 직접 관리를 해주었습니다. 해당 앱을 계속해서 유지보수하고, 만든 경험덕에 사실 큰 불편함은 느끼지 못 했지만, 느린 렌더링을 해결하기 위해서 전면으로 뜯어 고쳤습니다.

 

  네비게이션 해당 프로젝트에 도입하게 된 이유는 2가지 였습니다.

- 지적 호기심 : 각종 Android 강의나 최근 Github 자료에 Jetpack Navigation Graph가 함께 사용되어지는 케이스가 많이 있었고, 항상 궁금했습니다.

- Navigation의 특징 : Jetpack 네비게이션을 사용하는 이유는, 관심사의 분리라고 생각합니다. Document를 읽으면서, Naviagtion은 기존에 퍼져있던 인텐트들을 하나로 모아줄 수 있는 좋은 라이브러리라고 소개 되어있습니다. 그래서, 필요할 때마다 Intent를 생성하기 보다, 네비게이션 그래프를 그려서, 한 곳에서 인텐트를 관리하면 편하고, 눈에 띄게 잘 정리될 것이라고 생각을 했습니다.

2. 네비게이션 그래프 그리기

 메인화면에서 사용되어지는 fragment를 전부 분리시키고 난 후, navigation의 그래프를 그렸습니다.

리소스를 그리기 위해서는 아래의 그림과 같이, 리소스 폴더 이하에 Resource Type을 navigation으로 해서 만들면 됩니다.

 timetable_graph라는 네비게이션 그래프를 그렸고, 해당 그래프는 메인 화면에서 사용되어지는 프레그먼트를 등록시켜줬습니다.

//navigation tag에는 
app:startDestination="@id/fragment_name"

을 통해서, 시작하는 프레그먼트의 위치를 정해줍니다. 그리고, 네비게이션 이하에 프레그먼트를 등록시켜 줍니다.

프레그먼트는 id,name,label등의 속성을 갖고 있습니다. 만들어준 프레그먼트에는 이동해야하는 action들이 닮겨져 있어서, 해당 action을 만들어주게 되면, 네비게이션을 통해서 프레그먼트를 이동할 수 있습니다.

 그 외에도, 프레그먼트의 이동시에 애니메이션, 트렌지션 등 다양하게 사용할 수 있는 것이 특징이었습니다.

<fragment
	android:id="@+id/timetable_destination"
    andorid:name="com.my.path.MyFragment"
    android:label="FragmentName"
    tools:layout="@layout/my_fragment">
    <action
    	android:id="@+id/action_afragment_to_bfragment"
        app:destination="@id/b_fragment_destination"/>
</fragment>

중간 결과

FramgentContainerView & BottomNavigationView와 Navigation Graph를 연결하기

 만들어진 Navigation Graph를 실제 ui와 연결시키려다 보니, Fragment를 그려야하는 부분과, BottomNavigation이 Navigation Graph와 연결되어야 했습니다.

 

2-1. XML 그리기

 많은 레이아웃이 있었지만, 주로 봐야할 것은 위와 같습니다.

우선, 예제에는 Fragment를 navigation에 연결시켰는데, Lint가 FragmentContainerView로 대체하는 것 어떠니 라고, 추천을 해줘서, FragmentContainerView와 BottomNavigationView가 그래프와 연결됩니다.

 - FragmentContainerView : navGraph 속성 값에, navigation xml을 연결시켜줍니다.

 - BottomNavigationView : 바텀 네비게이션뷰는 menu에 연결 시킬 때, menu에 있는 id의 값이 navigation의 프레그먼트 id와 같아야만 자동 연결이 됩니다.

<androidx.fragment.app.FragmentContainerView
                android:id="@+id/fragment_main"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:defaultNavHost="true"
                app:layout_constraintBottom_toTopOf="@id/navigationBottom"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/abl_main_timetable"
                app:navGraph="@navigation/timetable_graph"
                tools:background="@color/main_color" />

<com.google.android.material.bottomnavigation.BottomNavigationView
                android:id="@+id/navigationBottom"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/realreal_white"
                app:elevation="@dimen/elevation_middle"
                app:itemIconSize="24dp"
                app:itemIconTint="@drawable/bnv_tab_item_foreground"
                app:itemTextAppearanceActive="@style/BottomNavigationView.Active"
                app:itemTextAppearanceInactive="@style/BottomNavigationView"
                app:itemTextColor="@drawable/bnv_tab_item_foreground"
                app:labelVisibilityMode="labeled"
                app:layout_constraintBottom_toBottomOf="parent"
                app:menu="@menu/navigation" />

2-2. Activity 코드

 FramgnetContainerView를 supportFragmentManager로 호출을 하고, NavHostFragment로 명명해주고, navigation에 navController를 넘겨줍니다.

val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_main) as NavHostFragment
binding.navigationBottom.setupWithNavController(navHostFragment.navController)

 이를 통해서, 구현상으로는 괜찮은 Jetpack Navigation을 활용해서 바텀 네비게이션과 navigation을 연결시켰습니다.

 

2-3. ViewPager2 코드

class TimeTableViewPagerAdapter(val activity: AppCompatActivity) : FragmentStateAdapter(activity) {
    override fun getItemCount() = 5

    override fun createFragment(position: Int): Fragment {
        when(position){
            0-> return TimeTableFragment.newInstance()
            1-> return TodoFragment.newInstance()
            2-> return ActivityFragment.newInstance()
            3 -> return CheckFragment.newInstance()
            4-> return GoalFragment.newInstance()
        }
        return throw Exception("You should not attach here.")
    }
}

3. 사용 후기

 사용하기 전

- 기능적으로는 이상이 없었던 구조였다.

- 안드로이드의 액티비티 구조보다는 기능에 관심이 많았을 때, 단순히 구현을 하기 위해서 '시간표', 'TODO', '대시보드', '함께 공부', '목표'가 모두 액티비티의 코드에 있었고, 하나의 xml에서 관리되면서 1600여줄이 넘는 xml 코드가 있었다.

- 많은 xml 코드는 데이터바인딩의 클래스가 만들어지지 못하는 지경에 이르렀다.

- 많은 액티비티 코드는 액티비티를 실행할 때, 느린 부트속도를 만들게 되었다.

- 많은 xml과 액티비티 코드는 ui를 전환하는 과정에서 visible, gone 지옥에 빠지게 만들었다.

 

 사용하면서

 - jetpack navigation은 docs에 많은 부분을 차지하고 있었고, 공부해볼만한 가치가 있다.

 - docs가 많이 있기 때문에 공부하는데 시간이 좀 걸린다.

 - single activity에 잘 어울린다.

 - bottom navigation에서 fragment -> activity -> fragment가 실행되었을 때, jetpack navigation을 예쁘게 그릴 수 있을 지는 모르겠다.

 - 점차 jetpack navigation을 사용해서 intent를 관리할 것이다.

 

 사용하고 난 후

 - jetpack navigation과 bottom navigation, viewpager2, fragment를 통해서 구조를 분리할 수 있었다.

 - 기존의 레거시 코드를 jetpack navigation을 활용한 코드로 변경하기 위해서 엄청난 코드 리팩토링을 진행하게 되었습니다.

 - 그 결과, 코드를 프레그먼트단위로 사용하기 편해졌습니다.

 - 프레그먼트 단위로 ui를 관리하면 되기 때문에, ui 관리가 용이해졌고, 데이터바인딩을 도입할 수 있었습니다.

 - jetpack navigation을 통해서 intent를 관리할 수 있게 되었습니다.

 - 사실, 기능적으로는 변한게 없습니다. 부팅 속도가 빨라졌을 지에 대해서는 앱을 배포해봐야 알겠습니다.

 

그럼 20000,

 

 모든 것은 jetpack navigation 덕은 아니지만, 액티비티의 코드가 1600여줄에서, 650줄로 줄어들었고, 사용하지 않은 레거시 코드를 많이 제거했습니다.

(좌) 과거의 액티비티 <-> (우) 현재의 액티비티