본문 바로가기
개발/Android

[Android] BottomNavigationView와 Navigation Graph를 같이 쓰는 경우

by 준그래머 2021. 2. 16.
반응형

 

BottomNavigationView와 Navigation Graph를 같이 쓰는 경우 아파치 소프트웨어 제단에서 만든 NavigationExtensions 클래스를 이용해서 구현하길 권장하고 있다. 그 이유는 시작점이 되는 Fragment에서 액티비티를 Finish 할 때 앱이 터진다고 하는데, 아직은 경험해보지 못해서 정확히는 모르겠고... 일단 권장하는 방식대로 앱을 구현해보자  

 

NavigationExtensions 파일은 아래에서 다운 받아준다. 

android-architecture-components/NavigationExtensions.kt at master · matthewzhang007/android-architecture-components (github.com)

 

matthewzhang007/android-architecture-components

Samples for Android Architecture Components. . Contribute to matthewzhang007/android-architecture-components development by creating an account on GitHub.

github.com

 

BottomNavigationView에서 사용할 만큼 Fragment를 생성해주자.

 

res 디렉터리에 navigation 디렉터리를 생성한다. 그 후에 Navigation Resource File을 생성하면 아래와 같은 메시지가 뜨는데, OK를 클릭한다.

 

그럼 자동으로 아래 라이브러리를 sync 및 build 해주는데, 이렇게 되면 아래에서 사용할 FragmentContainerView를 별다른 작업 없이 사용할 수 있다.

implementation 'androidx.navigation:navigation-ui:2.3.3'​

 

이제 각 각 필요한 만큼 navigation 파일을 생성해주고 아래처럼 각 각의 파일에 시작할 프래그먼트를 지정해준다.

 

이제 다시 res 디렉터리에 menu 디렉터리를 생성하고 bottom_nav.xml을 생성한다. 이때 주의할 점은 menu 아이템들에 아이디 값과 navigation의 아이디 값이 같아야 한다는 거다. 이거 안 해주면 아래와 같은 에러가 뜨는데, 한 시간 가량 헤맸다...

 

이거와 

이게 같아야 한다.

 

위에서 언급한 것처럼 Navigation Graph를 쓰기 위해 FragmentContainerView를 사용할 거고 BottomNavigationView에는 bottom_nav를 붙이고 labelVisibilityMode="labeled"를 통해 menu의 title을 선택하지 않아도 보일 수 있게 만들어준다. 

 

activity_main.xml

<LinearLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.act.MainAct">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:labelVisibilityMode="labeled"
        app:menu="@menu/bottom_nav" />

</LinearLayout>

 

 

MainAct.kt


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.yawtseb.bestway.R
import com.yawtseb.bestway.util.AnimationHelper.Companion.bottomPushDownAnimation
import com.yawtseb.bestway.util.AnimationHelper.Companion.bottomPushUpAnimation
import com.yawtseb.bestway.util.ShowMsg.Companion.showLog
import com.yawtseb.bestway.util.setupWithNavController

class MainAct : AppCompatActivity() {

    private val showBottomNavFrags = arrayListOf<Int>(R.id.homeFrag, R.id.dietFrag, R.id.mealTicketFrag, R.id.userFrag)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if(savedInstanceState == null) setUpBottomNavigationBar()

    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        setUpBottomNavigationBar()
    }

    private fun setUpBottomNavigationBar(){
        bottomNavigationView = findViewById(R.id.bottom_nav)
        val navGraphIds = listOf(R.navigation.navigation_home, R.navigation.navigation_diet, R.navigation.navigation_meal_ticket, R.navigation.navigation_user)

        val controller = bottomNavigationView.setupWithNavController(
            navGraphIds, supportFragmentManager, R.id.nav_host_container, intent
        )

        controller.observe(this, Observer { navController ->
            navController.addOnDestinationChangedListener { _, destination, _ ->
                if(showBottomNavFrags.contains(destination.id)) bottomNavigationView.visibility = View.VISIBLE
                else bottomNavigationView.visibility = View.GONE
            }
        })

    }

    companion object{
        private lateinit var bottomNavigationView: BottomNavigationView

        fun showBottomNav(){
            bottomPushUpAnimation(bottomNavigationView)
            bottomNavigationView.visibility = View.VISIBLE
        }

        fun hideBottomNav(){
            bottomPushDownAnimation(bottomNavigationView)
            bottomNavigationView.visibility = View.GONE
        }
    }

}

 

navigation 파일들을 리스트로 묶어서 선언해주고 NavigationExtenstions.kt 파일의 setupWithNavController()에 각 파라미터 타입에 맞는 객체들을 넣어 controller를 생성한다.

val navGraphIds = listOf(R.navigation.navigation_home, R.navigation.navigation_diet, 
	R.navigation.navigation_meal_ticket, R.navigation.navigation_user
)

val controller = bottomNavigationView.setupWithNavController(
	navGraphIds, supportFragmentManager, R.id.nav_host_container, intent
)

 

conroller는 NavController를 담고 있는 LiveData타입으로 lifecycler의 Observer를 사용해 NavController가 변할때 마다 감지해서 NavController를 반환해준다. 이때 Observer를 통해 navController에 리스너를 붙이고 BottomNavigationView를 상황에 맞게 컨트롤 해준다.

controller.observe(this, Observer { navController ->
	navController.addOnDestinationChangedListener { _, destination, _ ->
		if(showBottomNavFrags.contains(destination.id)) bottomNavigationView.visibility = View.VISIBLE
		else bottomNavigationView.visibility = View.GONE
	}
})

 

위에서 설명한 것처럼 showBottomNavFrags 리스트를 통해 BottomNavigationView의 보임 유무를 동적으로 처리하게 구현하였다. 이 리스트는 destination id가 showBottomNavFrags에 포함되는지 확인하여 처리하게 구현하였다.

private val showBottomNavFrags = arrayListOf<Int>(R.id.homeFrag, R.id.dietFrag, R.id.mealTicketFrag, R.id.userFrag)

 

마지막으로 Activity가 재시작될 때 상태값을 유지하기 위해서 아래와 같은 코드를 추가했다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        if(savedInstanceState == null) setUpBottomNavigationBar()
        
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        setUpBottomNavigationBar()
    }

 

만약 Appbar, Navigation Graph, BottomNavigationView를 전부 사용하려면 onSupportNavigateUp()을 이용해서 아래와 같이 선언해주고 Observer에서 상황에 맞게 Appbar를 수정해주면 된다.

 

override fun onSupportNavigateUp(): Boolean {
	return currentNavController?.value?.navigateUp() ?: false
}

 

아래는 참조한 사이트로 설명이 부족한 부분은 각 각의 페이지에서 보충하면 될거 같다.

 

참조한 사이트

 

TypeCastException in NavigationExtensions.kt · Issue #693 · android/architecture-components-samples

Hello I am working on a hobby app, using bottom navigation bar. I copied the NavigationExtensions.kt extension file from the NavigationAdvancedSample and I get this exception: kotlin.TypeCastExcept...

github.com

 

 

[Navigation] Android Jetpack Navigation + BottomNavigationView (1 / 2)

안녕하세요 허접샴푸입니다! Android Jetpack 라이브러리 중 하나인 Navigation에 대해서 알아보도록 하겠습니다. 최근 들어 안드로이드 앱들을 보면 모두 화면 하단에 메뉴가 있으며 중간 화면이 스위

programmar.tistory.com

 

 

 

AppCompatActivity  |  Android 개발자  |  Android Developers

From class androidx.fragment.app.FragmentActivity void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) Print the Activity's state into the given stream. FragmentManager getSupportFragmentManager() Return the FragmentManager for in

developer.android.com

 

 

onSaveInstanceState 활용하기 (Activity 데이터 유지) · snowdeer's Code Holic

onSaveInstanceState 활용하기 (Activity 데이터 유지) 16 Aug 2017 | Android onSaveInstanceState() 메소드를 이용하면 Activity가 종료될 때 데이터를 저장할 수 있습니다. 일반적으로 사용자가 정상적인 행동으로 Act

snowdeer.github.io