Android/Development Tips

KOIN FragmentFactory 사용하기

Nanamare 2020. 2. 29. 22:40
728x90

2020년 07월 12일 수정

 

아래 커밋을 보면 어떤식으로 FragmentFactory 를 사용할 수 있는지 알 수 있다.

https://github.com/Nanamare/CleanArchitecture/commit/52f173b7042583da288cc5a5b03e4b84b0f4d65c

 

[Feature] Change Fragment Factory logic · Nanamare/CleanArchitecture@52f173b

Permalink Browse files [Feature] Change Fragment Factory logic Loading branch information Showing 14 changed files with 129 additions and 51 deletions. +1 −1 .idea/misc.xml +1 −2 app/src/main/AndroidManifest.xml +3 −15 app/src/main/java/com/template/

github.com

    @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

 

Nanamare/android_architecture

Contribute to Nanamare/android_architecture development by creating an account on GitHub.

github.com

를 참고하면 된다.

728x90