Hanbit the Developer

[Kotlin] BottomNavigationView with multiple navigation 본문

Mobile/Android

[Kotlin] BottomNavigationView with multiple navigation

hanbikan 2022. 7. 19. 12:33

배경

저의 경우, BottomNavigationView로 나뉘는 각 item에서의 기능이 매우 상이하고 상호간 의존성도 없어서 아래와 같이 모듈화를 진행하였습니다. 이에 따라 각 item으로 fragment가 아니라 navigation을 두어야 했고, 이에 따라 일반적인 bottom navigation view와는 다른 구현을 했어야 했습니다.

출처: 다중 모듈 프로젝트를 위한 탐색 권장사항(developer.android.com)

 

구현

nav_home.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"
    android:id="@+id/nav_home"
    app:startDestination="@id/nav_a">

    <include app:graph="@navigation/nav_a"/>
    <include app:graph="@navigation/nav_b"/>
    <include app:graph="@navigation/nav_c"/>

</navigation>

<fragment/>가 아니라, 내비게이션을 include해야 합니다.

 

menu_home.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/nav_a"
        android:title="a"
        android:enabled="true"
        app:showAsAction="always"/>
    <item
        android:id="@+id/nav_b"
        android:title="b"
        android:enabled="true"
        app:showAsAction="always"/>
    <item
        android:id="@+id/nav_c"
        android:title="c"
        android:enabled="true"
        app:showAsAction="always"/>
</menu>

메뉴 item 또한 내비게이션을 참조합니다.

 

NavBottomNavigationView.kt

class NavBottomNavigationView: BottomNavigationView {
    constructor(context: Context): super(context, null)
    constructor(context: Context, attrs: AttributeSet?): super(context, attrs, com.google.android.material.R.attr.bottomNavigationStyle)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr, com.google.android.material.R.style.Widget_Design_BottomNavigationView)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int): super(context, attrs, defStyleAttr, defStyleRes)

    private var listeners: MutableList<(MenuItem) -> Unit> = mutableListOf()

    fun addOnItemSelectedListener(navController: NavController, listener: (MenuItem) -> Unit) {
        listeners.add(listener)
        setOnItemSelectedListener { item ->
            listeners.forEach { it.invoke(item) }
            return@setOnItemSelectedListener NavigationUI.onNavDestinationSelected(
                item,
                navController
            )
        }
    }
}

 

다음으로 BottomNavigationView의 setOnItemSelectedListener()로는 구현에 불편함을 느꼈고, 이를 확장하는 NavBottomNavigationView를 작성하여 addOnItemSelectedListener() 함수를 구현하였습니다.

특히 콜백 함수를 수행한 뒤, NavigationUI.onNavDestinationSelected()라는 함수를 호출해야만 정상적으로 작동하는 것을 확인하였습니다.(이걸 찾느라 삽질을 좀 했습니다🥲)

 

이후 추가로 문제를 발견하게 되었습니다. 해당 문제를 서술하기 위해, nav_menu 내부에서 fragment_main_menu, fragment_sub_menu 순서로 탐색을 진행했다고 가정해보겠습니다. 이 상태에서 하단 내비게이션 nav_menu item을 눌렀을 때 예상할 수 있는 일은, startDestination인 fragment_main_menu로 돌아가는 것입니다. 하지만 위 코드만 있었을 때 이에 대한 처리가 되지 않고 뷰에 변화가 없었습니다. 따라서 OnItemReselectedListener에서 navigate()를 호출하여 내비게이션을 초기화 처리해주었습니다. 완성된 코드는 다음과 같습니다.

/**
 * 각 Item이 Fragment가 아닌, Navigation으로 이루어졌을 때의 BottomNavigationView입니다.
 * 해당 커스텀 뷰는 다음과 같은 문제를 해결합니다.
 *  - 현재 위치한 Item을 눌렀을 경우 해당 Item 내부 페이지가 초기화가 되지 않는 문제
 *  - setOnItemSelectedListener()에 추가 동작을 설정하려면 NavigationUI.onNavDestinationSelected()을
 *    내부에서 호출해주어야 하는 불편함
 * 단, 내부 Item이 모두 Navigation라는 것을 가정하고 작성한 코드임에 주의해야 합니다.
 */
class NavBottomNavigationView: BottomNavigationView {
    constructor(context: Context): super(context, null)
    constructor(context: Context, attrs: AttributeSet?): super(context, attrs, com.google.android.material.R.attr.bottomNavigationStyle)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr, com.google.android.material.R.style.Widget_Design_BottomNavigationView)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int): super(context, attrs, defStyleAttr, defStyleRes)

    fun addOnItemSelectedListener(navController: NavController, listener: (MenuItem) -> Unit) {
        setOnItemSelectedListener { item ->
            startIndicatorAnimation(item.itemId)
            listener.invoke(item)

            return@setOnItemSelectedListener NavigationUI
                .onNavDestinationSelected(item, navController)
        }

        setOnItemReselectedListener { item ->
            navController.navigate(item.itemId) // 내비게이션의 초기 상태(startDestination)로 돌아갑니다.
        }
    }
}

 

 

activity_home.xml

<?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=".HomeActivity">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/bottomnavigationview"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_home"
            />


        <YOUR_PACKAGE_HERE.NavBottomNavigationView
            android:id="@+id/bottomnavigationview"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:labelVisibilityMode="labeled"
            app:menu="@menu/menu_home"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

마지막으로 위에서 생성한 NavBottomNavigationView를 xml 파일에 적용시킵니다. 기존 BottomNavigationView를 적용하는 것과 같은 방식으로 작성하면 됩니다.

 

 

References

https://developer.android.com/guide/navigation/navigation-multi-module

 

다중 모듈 프로젝트를 위한 탐색 권장사항  |  Android 개발자  |  Android Developers

다중 모듈 프로젝트를 위한 탐색 권장사항 탐색 그래프는 다음을 원하는 대로 조합하여 구성할 수 있습니다. 단일 대상(예: 대상) 일련의 관련 대상을 캡슐화하는 중첩 그래프 중첩된 것처럼 다

developer.android.com