2020년 07월 12일 수정
아래 커밋을 보면 어떤식으로 FragmentFactory 를 사용할 수 있는지 알 수 있다.
https://github.com/Nanamare/CleanArchitecture/commit/52f173b7042583da288cc5a5b03e4b84b0f4d65c
@NonNull
public final FragmentTransaction replace(@IdRes int containerViewId,
@NonNull Class<? extends Fragment> fragmentClass,
@Nullable Bundle args, @Nullable String tag) {
return replace(containerViewId, createFragment(fragmentClass, args), tag);
}
/**
* Calls {@link #add(int, Class, Bundle, String)} with a 0 containerViewId.
*/
@NonNull
public final FragmentTransaction add(@NonNull Class<? extends Fragment> fragmentClass,
@Nullable Bundle args, @Nullable String tag) {
return add(createFragment(fragmentClass, args), tag);
}
위와 같은 안드로이드 프레임워크에서 제공하는 Bundle 을 넘기는 함수를 이용하여 사용해야한다.
Example)
예를들어 이런식으로 사용하고 있습니다.
중간 함수 정의
fun FragmentManager.transact(action: FragmentTransaction.() -> Unit) {
beginTransaction().apply {
action()
}.commitAllowingStateLoss()
}
fun show(manager: FragmentManager, clazz: Class<out Fragment>, args: Bundle, tag: String?) {
manager.transact {
add(clazz, args, tag)
}
}
사용법
VideoFragment().show(
supportFragmentManager,
VideoFragment::class.java,
Bundle().apply {
putString(
VideoFragment.KEY_VIDEO_URL,
Uri.parse(VIDEO_URL).buildUpon().appendQueryParameter("v", it.key)
.build()
.toString()
)
},
VideoFragment::class.simpleName
)
val fragmentModule: Module = module {
fragment { VideoFragment() }
}
----------------------------------------------------------------------------------------------------------------------------
몇일 전에 KOIN 2.1.1 이 릴리즈 되었다!
1.x.x -> 2.x.x 로 넘어오면서 퍼포먼스(주입 시간), Scope 등이 개선되었지만, 또한 관심을 사로잡는 것이 fragmentFactory 를 사용하여,fragment 를 inject 시켜준다는 것이였다.
안드로이드 X 로 넘어오면서
기존 Fragment 를 개발할 때 기본 생성자를 싱글턴으로 만들고, 데이터를 넘길 때는 번들을 사용하는 방식(rotated orientation, changed configuration 대응책)에서 -> Fragment 생성자로 데이터를 넘겨도 FragmentFactory 에서 제공하는 instantiate 과정만 거치면, 문제가 없는 방식으로 변경이 되었다.
이 과정에서 가장 큰 변화는
companion object {
private const val KEY_COIN_TITLE = "key_coin_title"
private const val KEY_FILTER = "key_filter"
fun newInstance(marketList: List<String>, key: String): CoinFragment {
val args = Bundle()
args.putStringArrayList(KEY_COIN_TITLE, ArrayList(marketList))
args.putString(KEY_FILTER, key)
val fragment = CoinFragment()
fragment.arguments = args
return fragment
}
}
private fun loadBundle(savedInstanceState: Bundle?) {
val bundle = savedInstanceState ?: arguments ?: return
val list = bundle.getStringArrayList(KEY_COIN_TITLE)
val filter = bundle.getString(KEY_FILTER)
// TODO something
// .....
}
위와 같은 newInstance(args...), loadBundle(args...) 같은 데이터를 넣어주고 받아오는 함수들을 생략할 수 있게 되었다.
현재는 Sample 프로젝트 이기 때문에, companion object 안에 하나의 newInstance 메소드만 존재하지만, 앱 전체에서 사용하는 커스텀 다이얼로그라던지 데이터를 다양해서 보여줄 것 들이 많은 fragment 라면 엄청나게 많은 함수를 작성하게 되고, Bundle 로 넣어주고 사용하는 곳에서 받아서 사용하고 이런 코드들이 많이 생기게 되는데 이부분을 완전 줄일 수는 없지만 좀더 간편하게 사용할 수 있게 된다.
class CoinFragment(
private val marketList: List<String>,
private val key: String
) : BaseFragment<FragmentCoinListBinding>(R.layout.fragment_coin_list){
// TODO something
...
}
우리는 생성자에서 받아서 사용만 하면 된다.
그리고 KOIN DI 를 사용해서 더 간편하게 사용할 수 있도록 할 것 이다.
먼저 App gradle 에 라이브러리를 추가해준다.
ext.koin_android_version = '2.1.1'
// Koin AndroidX Scope features
implementation "org.koin:koin-androidx-scope:$koin_android_version"
// Koin AndroidX ViewModel features
implementation "org.koin:koin-androidx-viewmodel:$koin_android_version"
// Koin AndroidX Fragment features
implementation "org.koin:koin-androidx-fragment:$koin_android_version"
Application 클래스에 KoinAppication 초기화에 fragmentFactory() 를 추가하고 module 에 fragmentModule 을 추가해준다
startKoin {
androidLogger(Level.DEBUG)
androidContext(this@AppApplication)
fragmentFactory() // 추가
modules(
listOf(
networkModule,
repositoryModule,
viewModelModule,
apiManagerModule,
dataSourceModule,
fragmentModule // 추가
)
)
}
val fragmentModule = module {
// TODO fragment 추가해주기
}
그리고 중요한 것이 프래그먼트들의 호스트가 되는 액티비티에 fragmentFactory 를 초기화 시켜줘야한다.
우리는 KOIN 에서 제공해주는 함수를 사용할 것 이다.
override fun onCreate(savedInstanceState: Bundle?) {
setupKoinFragmentFactory() //추가
super.onCreate(savedInstanceState)
// TODO something...
}
setupKoinFragmentFactory() 는 내부적으로 fragmentFactory 를 상속받아 구현하고 있다.
class KoinFragmentFactory(val scope: Scope? = null) : FragmentFactory(), KoinComponent {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
val javaClass = Class.forName(className)
return scope?.let { it.get<Fragment>(javaClass) }
?: getKoin().get(javaClass.kotlin)
}
}
it.get<Fragment>(javaClass) 에서 우리가 원하는 프래그먼트와 프래그먼트가 필요로 하는 의존성들이 함께 주입된다.
만약 KoinFragmentFactory() 를 사용하고 싶지 않다면 이런식으로 하면된다
class CustomCoinFragmentFactory(private val dependency: Dependency) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
if (className == CoinFragment::class.java.name) {
return CoinFragment(dependency) // 내가 필요한 것들을 주입해서 사용하면된다.
}
return super.instantiate(classLoader, className)
}
}
예를 들어 fragment 에 하나 적용을 해보도록 하자
val fragmentModule = module {
fragment { (coinMap: LinkedHashMap<String, List<String>>) ->
MarketListFragment(coinMap)
}
}
MarketListFragment 는 key-String value-List<String> 파라미터를 받는다
MarketListFragment 는 MainActivity 에서 사용하고 있고 주입해보자
class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
private val marketListFragment by inject<MarketListFragment> { // 주입
parametersOf(coins)
}
val tlMarket
get() = binding.tlMarketList
private val marketVM: MarketViewModel by viewModel()
private var coins: LinkedHashMap<String, List<String>> = LinkedHashMap()
override fun onCreate(savedInstanceState: Bundle?) {
setupKoinFragmentFactory() // fragmentFactory 초기화
super.onCreate(savedInstanceState)
with(marketVM) {
onMarketClick()
market.observe(this@MainActivity, Observer {
coins = it // 주입때 DefinitionParameters 에 넣어줌
goToFragment(marketListFragment)
})
isLoadingObservable.observe(this@MainActivity, Observer {
when {
it -> showLoadingDialog()
else -> hideLoadingDialog()
}
})
}
}
}
사용하는 곳에서는 간단히 받아서 사용하면 된다
class MarketListFragment(private val marketMap: HashMap<String, List<String>>) : // 받아서 사용하기
BaseFragment<FragmentMarketListViewpagerBinding>(R.layout.fragment_market_list_viewpager) {
private val marketListViewPager: MarketListViewPager by lazy {
MarketListViewPager(
childFragmentManager
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView(binding)
}
private fun initView(binding: FragmentMarketListViewpagerBinding) {
binding.run {
marketMap.map {
marketListViewPager.addFragment(CoinFragment.newInstance(it.value, it.key), it.key)
}
coinViewPager.adapter = marketListViewPager
(requireActivity() as MainActivity).tlMarket.setupWithViewPager(coinViewPager)
}
}
}
코드가 많이 줄어든것 같다.
물론 아직까지는 장점만 있는 것은 아닌 것 같다.
by inject 해서 parameterOf 로 데이터를 넘겨 줄때 동적인 값들을 넘겨주는 부분이 프로퍼티를 사용해야해서,
동적인 값들은 기본생성자 + 번들로 처리하는 것이 편리할 수도 있을 것 같다. 이부분에 대해서도 해법이나 다양한 방법들이 나오지 않을까 싶다.
관련된 나머지 코드는 아래 링크를 참고하면 된다.
https://github.com/Nanamare/android_architecture/tree/master/Chapter-6/SampleProject
를 참고하면 된다.
'Android > Development Tips' 카테고리의 다른 글
다양한 뷰타입을 가지는 Recyclerview 만들기 (0) | 2020.12.26 |
---|---|
Clean Architecture (무비 앱) (0) | 2020.04.26 |
Custom view 에서 Koin 사용해 ViewModel 주입할 때 주의 할점 (0) | 2019.08.05 |
YUV420_8888 to NV21 (0) | 2019.04.16 |
중첩 프래그먼트와 제어 및 헤드리스 프래그먼트 (0) | 2018.12.13 |