Hanbit the Developer
[Kotlin] BottomNavigationView with multiple navigation 본문
배경
저의 경우, BottomNavigationView로 나뉘는 각 item에서의 기능이 매우 상이하고 상호간 의존성도 없어서 아래와 같이 모듈화를 진행하였습니다. 이에 따라 각 item으로 fragment가 아니라 navigation을 두어야 했고, 이에 따라 일반적인 bottom navigation view와는 다른 구현을 했어야 했습니다.
구현
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' 카테고리의 다른 글
[Kotlin] Custom Bottom Navigation With Animation (0) | 2022.08.01 |
---|---|
[Kotlin] Android Custom Gallery Image Picker 만들기 (0) | 2022.07.23 |
Android AAR Library 추가 방법 (0) | 2022.07.07 |
[Kotlin] Multiple View Types를 지원하는 RecyclerView 구현 (0) | 2022.07.02 |
JVM, DVM, ART(URL) (0) | 2022.03.27 |