Fragment, FragmentManager, FragmetTransaction에 대한 기본적인 내용은 Fragment와 FragmentManager, FragmentTransaction 글에 작성하였습니다. 이번 글에서는 BottomNavigationView을 사용하여 Fragment를 전환하는 방법에 대해 알아보겠습니다.
BottomNavigaionView에서 사용할 menu는 아래와 같이 생성합니다.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_one"
android:enabled="true"
android:icon="@android:drawable/ic_menu_add"
android:title="Fragment1" />
<item
android:id="@+id/menu_two"
android:enabled="true"
android:icon="@android:drawable/ic_menu_call"
android:title="Fragment2" />
<item
android:id="@+id/menu_three"
android:enabled="true"
android:icon="@android:drawable/ic_menu_camera"
android:title="Fragment3" />
</menu>
activity_main xml 파일은 아래와 같이 생성합니다. Fragment의 container는 FragmentContainerView로 지정하고, BottomNavigaionView의 menu 속성으로 위에서 생성한 xml 파일을 지정합니다.
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_main"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bnv_main"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bnv_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_navigation_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
class SampleFragment: Fragment() {
private var _binding: FragmentSampleBinding? = null
private val binding: FragmentSampleBinding
get() = _binding!!
private var count = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DataBindingUtil.inflate(inflater, R.layout.fragment_sample, container, false)
binding.apply {
tvName.text = requireArguments().getString("fragment_title")
tvCount.text = count.toString()
btnAddCount.setOnClickListener {
count++
tvCount.text = count.toString()
}
}
return binding.root
}
companion object {
fun newInstance(title: String) = SampleFragment().apply {
arguments = Bundle().apply {
putString("fragment_title", title)
}
}
}
}
Fragment의 코드입니다. DataBinding을 통해 레이아웃을 초기화하고 newInstance 메소드를 통해 Fragment 객체를 생성하도록 하였습니다. 그리고 버튼과 텍스트뷰를 배치하여 버튼을 누르면 count가 증가하고, 이 값이 텍스트뷰에 나타나도록 설정하였습니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.apply {
bnvMain.setOnItemSelectedListener { menuItem ->
changeFragment(menuItem.itemId)
true
}
}
// init
changeFragment(R.id.menu_one)
setContentView(binding.root)
}
// MenuItemid에 따른 Fragment 변경
private fun changeFragment(menuItemId: Int) {
val targetFragment = getFragment(menuItemId)
// FragmentKTX의 commit 메소드 사용
supportFragmentManager.commit {
// targetFragment로 변경
replace(R.id.container_main, targetFragment)
}
}
// MenuItemId에 따른 Fragment 생성
private fun getFragment(menuItemId: Int): Fragment {
val title = when(menuItemId) {
R.id.menu_one -> "Fragment1"
R.id.menu_two -> "Fragment2"
R.id.menu_three -> "Fragment3"
else -> throw IllegalArgumentException("Not found menu item")
}
return SampleFragment.newInstance(title)
}
}
MainActivity의 코드입니다. BottomNavigationView의 메뉴를 클릭하면 menuItem의 itemId를 changeFragment 함수에 인자로 넘겨줍니다. changeFragment의 함수는 getFragment 함수를 통해 프래그먼트 객체를 생성하고, replace를 통해서 Container에 설정된 프래그먼트를 getFragment를 통해 생성된 프래그먼트로 변경합니다.
화면을 보면 Fragment1에서 Count를 증가시키고 Fragment2로 갔다가 다시 돌아오면 Count가 초기화되는 것을 볼 수 있습니다. 그 이유는 위에 코드에서 프래그먼트를 replace 하기 때문에 프래그먼트를 전환할 때마다 새로운 프래그먼트를 생성하게 됩니다. 만약 이와 같은 동작을 의도해서 만든게 아니라면 한번 생성된 프래그먼트는 재사용하는 것이 효율적입니다.
우선 프래그먼트를 쉽게 관리하기 위해 enum class로 각 프래그먼트의 타입을 정의합니다.
enum class FragmentType(val fragmentTitle: String, val fragmentTag: String) {
FRAGMENT1("fragment1", "fragment1_tag"),
FRAGMENT2("fragment2", "fragment2_tag"),
FRAGMENT3("fragment3", "fragment3_tag");
}
class MainViewModel: ViewModel() {
private val _currentFragmentType = MutableLiveData(FragmentType.FRAGMENT1)
val currentFragmentType: LiveData<FragmentType>
get() = _currentFragmentType
// FragmentType에 따라 currentFragmentType 변경
fun setCurrentFragment(item: MenuItem): Boolean {
val menuItemId = item.itemId
val pageType = getPageType(menuItemId)
changeCurrentFragmentType(pageType)
return true
}
// menuItemId에 따른 FragmentType 반환
private fun getPageType(menuItemId: Int): FragmentType {
return when(menuItemId) {
R.id.menu_one -> FragmentType.FRAGMENT1
R.id.menu_two -> FragmentType.FRAGMENT2
R.id.menu_three -> FragmentType.FRAGMENT3
else -> throw IllegalArgumentException("Not found menu item")
}
}
// 현재 FragmentType과 비교하여 같으면 return, 다르면 변경
private fun changeCurrentFragmentType(fragmentType: FragmentType) {
if(currentFragmentType.value == fragmentType) return
_currentFragmentType.value = fragmentType
}
}
ViewModel과 LiveData를 사용해서 FragmentType(enum class)을 관리합니다. setCurrentFragment는 아래에서 살펴보겠지만 XML에서 BottomNavigationView에 연결된 BindingAdapter에 의해 호출되는 메소드입니다. BindingAdapter에 대해서 궁금하시다면 DataBinding - 2를 클릭해주세요.
setCurrentFragment를 통해 menuItem를 획득하면, getPageType은 menuItem의 ItemId에 따라서 FragmentType을 분기하고, changeCurrentFragmentType은 이 FragmentType과 LiveData에 저장된 FragmentType을 비교하여 LiveData의 값을 변경하는 함수입니다.
object ViewBinding {
@JvmStatic
@BindingAdapter("onNavigationItemSelected")
fun bindOnNavigationItemSelectedListener(
view: BottomNavigationView, listener: NavigationBarView.OnItemSelectedListener
) {
view.setOnItemSelectedListener(listener)
}
}
XML에 연결되는 BindingAdapter 함수입니다. 인자로 BindingAdapter가 연결되는 BottomNavigationView와 NavigationBarView.OnItemSelectedListener를 전달받습니다. 그리고 이 전달받은 BottomNavigationView에 setOnItemSelectedListener 메소드를 통해서 리스너를 등록하는 코드입니다.
<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">
<data>
<variable
name="viewModel"
type="kr.co.lee.bottomnavigationviewexample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_main"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bnv_main"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bnv_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:onNavigationItemSelected="@{viewModel::setCurrentFragment}"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_navigation_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
BottomNavigationView에 onNavigationItemSelected
라는 속성으로 BindingAdapter를 사용하였고, 인자로는 viewModel에 선언되었던 setCurrentFragment 함수를 주었습니다. 위에서 보았지만 setCurrentFragment는 NavigationBarView.OnItemSelectedListener에 선언된 함수인 onNavigationItemSelected와 시그니처가 똑같습니다. 따라서 해당 함수가 BottomNavigationView의 리스너로 등록되고, BottomNavigationView의 menu가 선택되면 ViewModel의 함수를 호출하는 것입니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val mainViewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.apply {
viewModel = mainViewModel
lifecycleOwner = this@MainActivity
}
// Observer 등록
mainViewModel.currentFragmentType.observe(this) {
// FragmentType이 변경되면 changeFragment에 FragmentType 인자로 넘김
changeFragment(it)
}
setContentView(binding.root)
}
// 현재 Fragment는 show, 나머지 Fragment는 hide
private fun changeFragment(fragmentType: FragmentType) {
// 현재 Fragemnt
var targetFragment = supportFragmentManager.findFragmentByTag(fragmentType.fragmentTag)
// Fragment Ktx의 commit 함수
supportFragmentManager.commit {
// 현재 Fragment가 null이라면
if (targetFragment == null) {
// getFragment를 호출하여 Fragment 획득
targetFragment = getFragment(fragmentType)
// 현재 Fragment 추가
add(R.id.container_main, targetFragment!!, fragmentType.fragmentTag)
}
// 현재 Fragment show
show(targetFragment!!)
// 나머지 Fragment hide
FragmentType.values()
.filterNot { it == fragmentType }
.forEach { type ->
supportFragmentManager.findFragmentByTag(type.fragmentTag)?.let {
hide(it)
}
}
}
}
// FragmentType을 받아서 Fragment 생성하는 함수
private fun getFragment(fragmentType: FragmentType): Fragment {
return SampleFragment.newInstance(fragmentType.fragmentTitle)
}
}
ViewModel에 저장된 FragmentType이 변경되면 changeFragment를 호출하여 현재의 FragmentType에 해당하는 Fragment는 show, 나머지 Fragment들은 hide 하게 됩니다.
본문의 예제 소스는 깃허브 링크에 있습니다.
참조
BottomNavigationView에서 Fragment 전환
틀린 부분은 댓글로 남겨주시면 수정하겠습니다..!!