Koin은 Dagger, Hilt처럼 안드로이드에서 사용되는 대표적인 DI 프레임워크 중 하나로, 순수 코틀린으로 작성되었으며 다른 DI 프레임워크보다 러닝커브가 낮고 경량화되었다.
Koin은 코틀린으로 작성되었기 때문에 코틀린 개발환경에 도입하기 쉬우며, ViewModel 주입을 간단하게 할 수 있는 별도의 라이브러리를 제공하는 장점 덕에 많이 사용된다. 다만 컴파일 타임에 주입대상을 선정하는 다른 DI 프레임워크에 비해 런타임에 Service Locating을 통해 동적으로 인스턴스를 주입하기 때문에 런타임 퍼포먼스가 다소 떨어질 수 있고, 이로 인해 런타임 오버헤드가 발생할 수 있다는 단점도 존재한다.
이 포스팅에서 Koin 셋업을 위한 Gradle 설정은 안드로이드 스튜디오 chipmunk 버전을 기준으로 한다.
// settings.gradle
repositories {
mavenCentral()
}
// build.gradle(module)
dependencies {
// Koin for Android
implementation "io.insert-koin:koin-android:$koin_version"
// Koin Test
testImplementation "io.insert-koin:koin-test:$koin_version"
testImplementation "io.insert-koin:koin-test-junit4:$koin_version"
}
Koin은 Hilt처럼 Annotation을 사용하지 않고, Kotlin DSL을 사용해 개발자가 좀 더 편리하게 의존성을 주입할 수 있도록 API를 제공한다.
@Module
@InstallIn(ViewModelComponent::class)
internal object RepoModule {
@Provides
@ViewModelScoped
fun provideRepo() : Repository = Repository()
}
val dbModule = module {
single {
Repository())
}
}
val viewModelModule = module {
viewModel {
MainViewModel(get())
}
}
Koin은 Application DSL과 Module DSL을 제공하는데, Application DSL로 Koin 컨테이너의 구성을, Module DSL로 주입되어야 하는 컴포넌트를 표현할 수 있다.
App.kt
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin{
androidContext(this@App)
modules(
dbModule,
viewModelModule
)
}
}
}
안드로이드 환경에서 Koin을 사용하기 위해선 먼저 Application을 상속한 클래스가 필요하며, 해당 클래스 내에서 Koin 컨테이너를 실행하기 위해 다음의 DSL을 사용할 수 있다.
startKoin { }
: Koin 컨테이너를 실행하기 위한 진입점으로, 실행할 모듈 목록이 설정되어 있어야 한다.
Koin 컨테이너 인스턴스를 구성하기 위해선 다음 중 필요한 것들을 적절히 사용하면 된다.
androidLogger( )
: Koin용 안드로이드 Logger를 설정한다.
androidContext(this@App)
: Koin 컨테이너에 안드로이드 컨텍스트를 주입한다.
modules( )
: koin 컨테이너에 로드할 모듈들을 설정한다.
androidFileProperties( )
: assets/koin.properties 파일의 Koin properties를 사용하여 키/값을 저장한다.
생성한 컨테이너 클래스는 메니페스트 파일에 등록해야 한다.
AndroidManifest.xml
<application
...
android:name=".App">
</application>
MainViewModel.kt
class ViewModel(private val dao: Dao) : ViewModel(){
...
}
Module.kt
val dbModule = module {
fun provideDatabase(application: Application) : AppDatabase {
return Room.databaseBuilder(application, AppDatabase::class.java, "EXAMPLE_DB")
.fallbackToDestructiveMigration()
.build()
}
fun provideDao(database: AppDatabase) : Dao {
return database.Dao
}
single {
provideDatabase(androidApplication())
}
single {
provideDao(get())
}
}
val viewModelModule = module {
viewModel {
MainViewModel(get())
}
}
Koin 모듈은 우리가 주입하고 결합할 모듈들에 대한 명세를 수집한다. 새로운 모듈을 생성하기 위해선 다음의 Module DSL을 사용해야 한다.
module { }
: Koin 모듈을 생성한다.
모듈 내의 객체들을 정의하기 위해선 다음의 DSL을 용도에 맞게 사용해야 한다.
factory { }
: 요청할 때마다 매번 새로운 객체를 생성한다. factory로 제공되는 객체는 컨테이너에 저장하지 않기 때문에 다시 참조할 수 있다.
single{ }
: 앱이 실행되는 동안 계속 유지되는 싱글톤 객체를 생성한다. 싱글톤 scope로 해당 객체를 만들어서 사용할 수 있다. 보통 repository, retrofit, db등을 사용할 때 사용한다.
viewModel { }
: viewModel 키워드로 모듈을 등록하면 Koin이 해당 ViewModel을 ViewModelFactory에 등록한 뒤 현재 컴포넌트와 바인딩하고, 주입 받을 때도 viewModelFactory에서 해당 ViewModel 객체를 불러온다.
get()
: 모듈에 제공된 객체들 중 해당 부분에 들어갈 수 있는 객체를 찾아 넣는다.
이제 정의해둔 모듈들을 액티비티, 프래그먼트, 서비스와 같은 안드로이드 컴포넌트에 주입해보자. 모듈을 컴포넌트에 주입하기 위해 사용하는 메서드는 다음과 같다.
// Example
class MainFragment : Fragment() {
private val presenter : Presenter by inject()
private val presenter : Presenter = get()
private val viewModel : ViewModel by viewModel()
}
by inject()
: get과 같이 알맞은 의존성을 주입하나, val 변수에만 사용할 수 있다. lazy 방식의 주입으로, 해당 객체가 사용되는 시점에 의존성을 주입한다.
get()
: 해당 코드 런타임에 바로 객체를 주입한다.
by viewModel()
: ViewModel 객체를 lazy하게 주입한다.
getViewModel()
: by viewModel()과 다르게 바로 ViewModel을 주입한다.
또한 하나의 ViewModel 인스턴스는 여러 프래그먼트와 호스트 액티비티 사이에서 공유되어 사용될 수 있다. 공유된 ViewModel은 프래그먼트에서 다음의 메서드를 사용해서 주입될 수 있다.
by sharedViewModel()
: 공유된 ViewModel 인스턴스를 lazy하게 주입한다.
getSharedViewModel()
: by sharedViewModel()과 다르게 바로 공유된 ViewModel을 주입한다.
val weatherAppModule = module {
viewModel { WeatherViewModel(get(), get()) }
}
class WeatherActivity : AppCompatActivity() {
/*
* Declare WeatherViewModel with Koin and allow constructor dependency injection
*/
private val weatherViewModel by viewModel<WeatherViewModel>()
}
class WeatherHeaderFragment : Fragment() {
/*
* Declare shared WeatherViewModel with WeatherActivity
*/
private val weatherViewModel by sharedViewModel<WeatherViewModel>()
}
class WeatherListFragment : Fragment() {
/*
* Declare shared WeatherViewModel with WeatherActivity
*/
private val weatherViewModel by sharedViewModel<WeatherViewModel>()
}
액티비티나 프래그먼트가 아닌 일반 클래스에서도 의존성 주입이 필요할 때가 있다. 하지만 일반 클래스에서는 get()이나 by inject()를 사용해 의존성 주입을 할 수 없다. 안드로이드에서 Koin을 사용할 시 컨테이너는 안드로이드 컴포넌트의 생명주기에 맞추어 생성과 파괴가 되도록 만들어져야 한다. 액티비티나 프래그먼트 내부에서 컨테이너를 생성한 경우 해당하는 생명주기를 따르겠지만, 일반 클래스의 경우 생명주기가 없기 때문이다.
여튼 이렇게 일반 클래스에서 Koin을 사용하기 위해 KoinComponent라는 것을 제공하고 있다. KoinComponent를 사용하면 by inject(), get() 등을 사용해 의존성을 주입할 수 있다.
class MyComponent : KoinComponent {
// lazy inject Koin instance
val myService : MyService by inject()
// eager inject Koin instance
val myService : MyService = get()
}
하지만 다음의 StackOverFlow, Medium post를 확인해보면, 생성자 파라미터를 통해 의존성을 주입받을 수 없는 상황을 제외하면 이 방법은 권장되지 않는다. 따라서 KoinComponent보단, 액티비티나 프래그먼트에서 Koin을 사용해야 하는 클래스를 생성할 때 생성자를 통해 객체를 주입하는 것이 좋다.
class MainActivity : AppCompatActivity() {
private val dataStoreModule : DataStoreModule by inject()
override fun onCreate(savedInstance: Bundle){
super.onCreate(savedInstanceState)
...
Aclass(dataStoreModule)
}
}
class Aclass(private val dataStoreModule: DataStoreModule) {
...
}
val androidModule = module {
single {
MyService()
}
}
@Composable
fun App() {
val myService = get<MyService>()
val myService by inject<MyService>()
}
module {
viewModel {
MyViewModel()
}
}
@Composable
fun App() {
val vm = getViewModel<MyViewModel>()
val vm by viewModel<MyViewModel>()
}
https://blog.banksalad.com/tech/migrate-from-koin-to-hilt/
https://kotlinworld.com/123
https://jaejong.tistory.com/153
https://medium.com/swlh/dependency-injection-with-koin-6b6364dc8dba