Programming/Android

[Android] ViewPager2 + BottomNavigation 으로 Fragment 관리하기

YK Choi 2021. 5. 16. 02:14

Jetpack에서 나온 BottomNavigation은 Fragment 전환을 쉽게 도와준다.

 

지금까지 내가 쓰던 방식은 전환될 Fragment를 xml파일에서 <fragment> 를 썼었는데 바텀네비게이션을 클릭할 때마다 fragment를 재생성하는 단점이 있었다. Fragment을 가진 Activity에서 재생성 안하도록 제어하는 방법을 찾다가 ViewPager2를 알게 되었고, 이 문제를 쉽고 명료하게 해결해주었다.

 

우선 ViewPager의 dependency를 추가해야 한다.

implementation "androidx.viewpager2:viewpager2:1.0.0"

 

1. BottomNavigation에 사용될 menu를 정의한다.

menu/menu_bottom_navigation.xml

2. activity의 layout을 정의한다. ViewPager2와 BottomNavigation이 나온 부분만 잘랐다.

activity_main.xml

3. home, accout, favorite 각각의 Fragment 의 layout도 정의한 다음 Activity를 아래와 같이 쓴다.

 MainActivity의 전체코드는 이렇다.

 

추가)

setOnNavigationItemSelectedListener가 material 1.4.0 에서 deprecated되었다. 

이거 대신 setOnItemSelectedListener를 쓰면 된다. 사용방법은 동일하다.

 

class MainActivity : AppCompatActivity() {

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

        main_view_pager.isUserInputEnabled = false
        main_view_pager.adapter = PagerAdapter(supportFragmentManager,lifecycle)
        main_view_pager.registerOnPageChangeCallback(PageChangeCallback())
        main_bottom_navigation.setOnNavigationItemSelectedListener { navigationSelected(it) }
    }

    private fun navigationSelected(item: MenuItem): Boolean {
        val checked = item.setChecked(true)
        when (checked.itemId) {
            R.id.home_screen -> {
                main_view_pager.currentItem = 0
                return true
            }
            R.id.account_screen -> {
                main_view_pager.currentItem = 1
                return true
            }
            R.id.favorite_screen -> {
                main_view_pager.currentItem = 2
                return true
            }
        }
        return false
    }

    private inner class PagerAdapter(fm: FragmentManager, lc: Lifecycle): FragmentStateAdapter(fm, lc) {
        override fun getItemCount() = 3
        override fun createFragment(position: Int): Fragment {
            return when (position) {
                0 -> HomeFragment()
                1 -> AccountFragment()
                2 -> FavoriteFragment()
                else -> error("no such position: $position")
            }
        }
    }

    private inner class PageChangeCallback: ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            main_bottom_navigation.selectedItemId = when (position) {
                0 -> R.id.home_screen
                1 -> R.id.account_screen
                2 -> R.id.favorite_screen
                else -> error("no such position: $position")
            }
        }
    }
}

 

이제 설명을 해보자면

PagerAdapter 클래스를 만들어 ViewPager를 감싸는 어댑터를 만드는것이다.

PagerAdapter는 추상클래스인 FragmentStateAdapter를 상속받아 getItemCount(), createFragment()를 재정의한다.

 

FragmentStateAdapter를 들여다보면 RecyclerView.Adapter를 상속하고 있다. 덕분에 프래그먼트를 매번 재생성하지 않고 재사용할 수 있다.

 

여기서 StatefulAdapter도 implement하고 있는데  fragment에서 onSaveInstanceState() 와 onRestoreInstanceState(Parcelable) 를 사용할 수 있게 하기 위해서라고 한다. (StatefulAdapter 관련 링크)

FragmentStateAdapter 소스코드 일부
StatefulAdapter 소스코드

 

PageChangedCallback은 Fragment가 전환되었을 때 호출되는 콜백함수다.

navigationSelected()를 리스너로 넣어주는 것은 바텀네비게이션이 클릭되었을때 Fragment를 전환시켜주기 위해서이다.

 

PageChangedCallback이 그럼 굳이 필요 없지 않은가?라고 잠깐 생각도 했었지만 ViewPager2는 슬라이드(touch swipe)로 Fragment 전환이 가능해서 navigationSelected()보다 PageChangedCallback이 먼저 호출될 수도 있기 때문에 필요할수 있다.

 

터치 스와이프로 Fragment 전환을 없애고 싶으면 ViewPager2.isUserInputEnabled 를 false로 두면 된다.

 

전체 소스코드

https://github.com/yeon-kyu/Android_BlindCommunity_refactor

 

참고한 사이트

https://roomedia.tistory.com/entry/kotlin-%EC%9D%B4%EC%8A%88-8-BottomNavigationView-ViewPager2-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%93%9C-%EB%A9%94%EB%89%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0