사이드 프로젝트를 하거나 추후 회사 내에서 신규로 앱을 개발해야할 상황이 올 수 있다. 그럴 때 앱 개발을 시작하기 위한 초기 세팅들이 필요하다.
예를 들면, MVVM패턴을 구성하기 위한 세팅 중 DataBinding을 설정하거나 ViewModel class, Repository class, Api Interface 등의 구조를 잡아놓아야 한다.
더 나아가 앱의 구성이 BottomNavigation과 그 상단에 Fragment로 구성되는 경우 이에 따른 Fragment BackStack조치도 해야한다.
이 경우가 생각보다 많은 시간을 잡아먹는다 생각되어 초기 세팅을 만들어 봤다. 그 중, BottomNavigation과 Fragment의 싱크를 맞추고 Fragment BackStack까지 감안한 초기 설정을 만들었다.
기능의 추가가 있을 때 기존 코드의 수정이 이뤄지지 않고 코드의 수정이 이루어질 수 있게하는 객체지향 SOLID원칙 중 하나
위 기능을 구현하기 위해 BottomNavigation 함수를 만들었다. 그때 추후 변경사항을 예상해본다면 메인 화면 탭이 추가/제거되는 경우가 있다. 이럴 경우, BottomNavigation Extention함수의 추가 수정이 이루어지지 않도록 설계했다. 즉, 기능의 변경이 발생한다 하더라도 기존 로직은 수정할 필요가 없는 것이다.
BottomNav부분과 그에 대응되는 Fragment설정은 오로지 DI모듈 내에서만 하면 된다. 또한 여기서 DI방식은 Koin으로 했음을 밝혀둔다.
DI Container
val viewModule = module { single { SparseArray<Fragment>().apply { append(R.id.firstTabFragment, FirstTabFragment()) append(R.id.secondTabFragment, SecondTabFragment()) append(R.id.thirdTabFragment, ThirdTabFragment()) append(R.id.forthTabFragment, ForthTabFragment()) } } }
간단 설명을 하자면, 왼쪽은 menu.xml에 있는 fragmentId들이다. 이 녀석들은 BottomNavigation의 구성하는 역할을 맡는다.
오른쪽의 Fragment들은 BottomNavigation과 대응되는 Fragment들이다. 이 코드들만 수정해주면 되기에 개인적으로 OCP원칙을 만족시켰다고 생각한다.
아래는 전체 코드이다. 이후 한줄 한줄 풀이해볼까 한다.
package com.example.starter.ext
import android.util.SparseArray
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.example.starter.R
import com.google.android.material.bottomnavigation.BottomNavigationView
fun BottomNavigationView.init(
fragmentManager: FragmentManager,
containerId: Int,
idToFragmentMap: SparseArray<Fragment>
) {
fragmentManager.beginTransaction(containerId, R.id.firstTabFragment, idToFragmentMap[R.id.firstTabFragment])
setOnItemSelectedListener { item ->
if (item.itemId == selectedItemId) {
return@setOnItemSelectedListener true
}
fragmentManager.beginTransaction(
containerId = containerId,
targetId = item.itemId,
targetFragment = idToFragmentMap[item.itemId])
true
}
fragmentManager.addOnBackStackChangedListener {
val currentVisibleFragment = fragmentManager.getBackStackEntryAt(fragmentManager.backStackEntryCount.minus(1)).name?.toInt() ?: R.id.firstTabFragment
menu.findItem(currentVisibleFragment).isChecked = true
}
}
private fun FragmentManager.beginTransaction(containerId: Int, targetId: Int, targetFragment: Fragment) {
beginTransaction()
.replace(containerId, targetFragment, "$targetId")
.addToBackStack("$targetId")
.setReorderingAllowed(true)
.setPrimaryNavigationFragment(targetFragment)
.commit()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dataBinding.run {
bottomNavigation.init(
fragmentManager = supportFragmentManager,
containerId = mainNavFragment.id,
idToFragmentMap = idToFragmentMap)
}
}
MainActivity에서는 위와 같이 호출한다. 필요한 코드는 fragmentManager, 화면 콘텐츠를 보여주는 중앙 fragmentId 그 후 위에서 설명한 idToFragmentMap이다.
fun BottomNavigationView.init(
fragmentManager: FragmentManager,
containerId: Int,
idToFragmentMap: SparseArray<Fragment>
)
동일하니 pass
결론부터 말하면 BackStack을 무한히 쌓이게 해 놓았다. 그 이유는 우리가 보통 웹 사이트 서핑을 할 때 뒤로가기를 누르는데, 이땐 우리가 이전에 봤던 페이지들이 나오도록 기대하기 때문이다.
어차피 앱이 종료되면 기존에 쌓였던 Fragment객체들은 사라진다. 또한 앱을 어차피 사용하는 중에는 방금 말한 이유로 굳이 지워줄 필요가 없다고 생각한다.
fragmentManager.beginTransaction(containerId, R.id.firstTabFragment, idToFragmentMap[R.id.firstTabFragment])
보통 앱을 처음 시작하면 BottomNavigation의 첫 번째 탭에 해당하는 fragment를 보여주게 된다. 이에 맞게 설정한 코드이다.
setOnItemSelectedListener { item ->
if (item.itemId == selectedItemId) {
return@setOnItemSelectedListener true
}
fragmentManager.beginTransaction(
containerId = containerId,
targetId = item.itemId,
targetFragment = idToFragmentMap[item.itemId])
true
}
이 후, 하단 탭을 클릭했을 때, 그에 맞는 fragment를 전환해줘야 한다. 이에 setOnItemSelectedListener 콜백 함수를 구현하여 Fragment가 바뀔 수 있게 해놓았다.
첫 번째 조건문이 있다. 그 조건문은 해당 탭이 2번 이상 클릭되었을 땐 화면전환을 하지 않도록 해놓은 코드이다. 만약 위 코드가 없다면 동일한 fragment BackStack이 무한히 쌓일 것이다. 그래서 설정해 놓은 코드이다.
하지만 여기서 beginTransaction을 자세히 보면, 파라미터가 기존 android api와는 다르다는게 느껴질 것이다. 이 부분은 2번 쓰이므로 또 다른 private함수로 별도 정의해 놓았다.
private fun FragmentManager.beginTransaction(containerId: Int, targetId: Int, targetFragment: Fragment) {
beginTransaction()
.replace(containerId, targetFragment, "$targetId")
.addToBackStack("$targetId")
.setReorderingAllowed(true)
.setPrimaryNavigationFragment(targetFragment)
.commit()
}
화면을 전환할 때, 기존의 화면은 제거해주고 새로운 화면으로 전환해주기 위해 replace함수를 사용하였다. 그리고 전환할 때, 어떤 화면으로 전환해줄 것인지에 대한 프래그먼트 설정(=targetFragment)와 해당 프래그먼트 id설정(="$targetId")설정을 해주었다.
그 후, setReorderingAllowed와 setPrimaryNavigation설정 코드가 있는데 이 부분에 대한 자세한 설명은 다음 포스팅을 참고하길 바란다.
이로써 어떤 앱을 신규로 개발하든 상관 없이 앱 개발 초기 설정을 마쳤다고 생각한다. 물론 미래는 예측할 수 없기에 100%라고 할 순 없지만, 지금 내 생각엔 100%라고 조심스레 생각해 본다.
이 글을 통해서 Git에 코드를 확인하러 가는 분들이 분명 있을거라 생각하여 짧게 가이드를 남겨볼까 한다.
보는법 순서
1. MainActivity에서 init함수 선언부를 본다.
2. 그 중 파라미터로 'idToFragmentMap'이 있다. 이 부분은 Koin Di Container(=viewModeuls.kt)로 분리해놓았다. 해당 부분을 본다.
3. init함수를 본다.