안드로이드 앱을 개발할때 Dagger 와 같은 Dependency Injection(이하 DI)을 사용하게 됩니다. 각 컴포넌트간의 의존성을 외부 컨테이너에서 관리하는 방식을 통해 코드 재사용성을 높이고 Unit Test도 편하게 할 수 있게 되는 장점을 가지고 있습니다. Koin은 코틀린 개발자를 위한 실용적인 API제공을 하는 경량화된 의존성 주입 프레임워크입니다.
Application 클래스를 상속받는 YUMarketApplication 클래스를 생성합니다. onCreate() 에서 startKoin { ... } 을 호출하고 모듈을 값을 넣어줬습니다.
class YUMarketApplication : Application() {
override fun onCreate() {
super.onCreate()
appContext = this
startKoin {
androidLogger(Level.ERROR)
androidContext(this@YUMarketApplication)
modules(appModule)
}
}
}
AndroidManifest.xml 에 android:name=".YUMarketApplication" 을 통해 만들어준 Application 클래스를 등록해야 합니다. 꼭 등록해야합니다!
val appModule = module {
single { Dispatchers.IO }
single { Dispatchers.Main }
single<MapRepository> { DefaultMapRepository(get(), get()) }
single<HomeRepository> { DefaultHomeRepository() }
single<ResourcesProvider> { DefaultResourcesProvider(androidContext()) }
single { buildOkHttpClient() }
single { provideGsonConverterFactory() }
single(named("map")) { provideMapRetrofit(get(), get()) }
single { provideMapApiService(get(qualifier = named("map"))) }
viewModel { (homeListCategory: HomeListCategory) -> HomeListViewModel(homeListCategory, get()) }
viewModel { HomeMainViewModel(get()) }
viewModel { MainViewModel(get()) }
}
module : 주입받고자 하는 객체를 선언하고 변수에 저장을 해줍니다.
val appModule = module {}
get() : 컴포넌트 내에서 이미 생성된 의존성을 주입 받을 수 있습니다. 주입하는 반환 클래스가 한개일 경우 자동으로 매칭하여 반환해 줍니다.
예시)
single { provideMapApiService(get(qualifier = named("map"))) }
fun provideMapApiService(retrofit: Retrofit): MapApiService {
return retrofit.create(MapApiService::class.java)
}
MapApiService로 반환 하는경우는 provideMapApiService 밖에 없으므로 get()을 하게되면 알아서
DefaultMapRepository의 MapApiService에 싱클톤으로 만들어진 provideMapApiService 객체가
자동으로 의존성 주입이 되게 됩니다.
single<MapRepository> { DefaultMapRepository(get(), get()) }
class DefaultMapRepository(
private val mapApiService: MapApiService,
private val ioDispatcher: CoroutineDispatcher
) {}
single : 앱이 실행되는 동안 계속 유지되는 싱글톤 객체를 생성합니다.
//Coroutine Dispatcher - Main, IO
single { Dispatchers.IO }
single { Dispatchers.Main }
//Repository
single<MapRepository> { DefaultMapRepository(get(), get()) }
single<HomeRepository> { DefaultHomeRepository() }
//CustomProvider
single<ResourcesProvider> { DefaultResourcesProvider(androidContext()) }
//Network
single { buildOkHttpClient() }
single { provideGsonConverterFactory() }
single(named("map")) { provideMapRetrofit(get(), get()) }
single { provideMapApiService(get(qualifier = named("map"))) }
factory : 주입될 대상 모듈의 인스턴스를 호출 하는 시점마다 생성하여 주입 합니다.
viewModel : 요청시 매번 새로운 객체를 생성하는 것은 factory와 동일하나 AAC viewModel 용으로 사용하고 있다고 Koin에 명시하는 역할로 추측됩니다. 자세한 내용은 아래에서 추가로 다루겠습니다.
Koin Module에 viewModel혹은 factory를 선언한 다음에 by viewModel()를 사용하면 ViewModelProvider를 사용하지 않고 ViewModel을 생성할 수 있습니다.
( ViewModelProvider에 대해서는 따로 하나의 주제로 글을 작성하겠습니다. )
viewModel 혹은 factory를 위와 같이 Koin Module에 선언을 해줍니다.
: ViewModel()를 상속 받은 경우에만 viewModel을 Koin Module에 선언이 가능합니다.
val appModule = module {
viewModel { (homeListCategory: HomeListCategory) -> HomeListViewModel(homeListCategory, get()) }
viewModel { HomeMainViewModel(get()) }
viewModel { MainViewModel(get()) }
factory { (homeListCategory: HomeListCategory) -> HomeListViewModel(homeListCategory, get()) }
factory { HomeMainViewModel(get()) }
factory { MainViewModel(get()) }
}
Koin에서 module {}에 선언된 viewModel() 함수를 살펴보면 생성될 ViewModel의 인스턴스를 아래 함수 내용을 보면 factory()를 통해 관리 하게됩니다. factory()로 주입될 대상 모듈의 인스턴스를 호출 하는 시점마다 생성하여 주입 합니다. 그래서 factory()나 viewModel() 둘 중 하나로 선언해도 결과론 적으로는 차이가 없습니다.
inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
override: Boolean = false,
noinline definition: Definition<T>
): BeanDefinition<T> {
val beanDefinition = factory(qualifier, override, definition)
beanDefinition.setIsViewModel()
return beanDefinition
}
by viewModel() : by를 통해 viewModel() 함수에 초기화를 위임 하여 ViewModel 인스턴스를 주입 받습니다.
getViewModel() : ViewModel 인스턴스를 직접 가져옵니다.
예시)
class SomeFragment: Fragment() {
val viewModel1: firstViewModel by viewModel()
val viewModel2: secondViewModel = getViewModel()
...
}
by viewModel() 과 getViewModel() 의 차이점은 lazy 위임 여부 입니다. 아래 코드를 통해 확인할 수 있습니다.
< viewModel >
inline fun <reified T : ViewModel> ViewModelStoreOwner.viewModel(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) { getViewModel<T>(qualifier, parameters) }
}
< getViewModel >
inline fun <reified T : ViewModel> ViewModelStoreOwner.getViewModel(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T {
return getViewModel(T::class, qualifier, parameters)
}
Koin은 ViewModel 의 주입 이전에 인스턴스 생성시 생성자에 변수를 선언하는 작업을 할 수 있습니다. 추후에 viewModel을 의존성 주입을 할 때 parametersOf() 통해 파라미터를 전달 할 수 있습니다.
val appModule = module {
viewModel { (homeListCategory: HomeListCategory) -> HomeListViewModel(homeListCategory, get()) }
}
override val viewModel by viewModel<HomeListViewModel> {
parametersOf(homeListCategory)
}
주소 하나로 데이터를 공유하면서 공통으로 쓰일 ViewModel 인스턴스는 액티비티에 있는 여러개의 프래그먼트 에서 같이 주입되어 사용 할 수 있습니다.
class FirstFragment: Fragment() {
val viewModel: CommonViewModel by shareViewModel()
}
class SecondFragment: Fragment() {
val viewModel: CommonViewModel by shareViewModel()
}