ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] Koin으로 의존성 주입하기
    Mobile Engineering 2020. 4. 19. 15:19

    요즘 안드로이드 프로젝트를 수행하면서 의존성 주입(DI - Dependency Injection)은 필수라 할 수 있습니다.

    그래서 코틀린 환경에서 떠오르고 있는 DI Framework인 Koin에 대해서 이야기하고자 합니다.

     

    1. Koin 세팅

    최상단의 Project Gradle(ROOT/build.gradle)에 아래와 같이 설정을 추가합니다.

    buildscript {
    	dependencies {
        	classpath "org.koin:koin-gradle-plugin:$koin_version"
        }
    }

     

    그리고 Koin을 사용하는 Module의 Gradle(app/build.gradle)에 아래와 같이 설정을 추가합니다.

    apply plugin: 'koin'
    
    android {
        packagingOptions {
            exclude 'META-INF/*.kotlin_module'
        }
    }
    
    dependencies {
        implementation "org.koin:koin-core:$koin_version"
        implementation "org.koin:koin-core-ext:$koin_version"
        testImplementation "org.koin:koin-test:$koin_version"
        implementation "org.koin:koin-androidx-scope:$koin_version"
        implementation "org.koin:koin-androidx-viewmodel:$koin_version"
        implementation "org.koin:koin-androidx-fragment:$koin_version"
    }

     

    사실상 여기서 모든 세팅은 끝마쳤지만, Koin의 공식 문서에 언급되지 않은 packagingOptions에 대해서 궁금하실 겁니다.

    packagingOptions에서 exclude가 없으면 'More than one file was found with OS independent path 'META-INF/kotlinx-coroutines-core.kotlin_module' 메시지가 나오면서 Exception이 발생합니다. 그래서 이를 제거하기 위해 META-INF 하단에 있는 *.kotlin_module 파일을 참조하지 않도록 설정한 것입니다. 

     

    2. 코드 생성

    도시 이름에 대한 Repository와 UseCase를 만들도록 하겠습니다.

    // CityRepository.kt
    
    package com.hans.android.koinexam.data.repository
    
    class CityRepository {
        private val cities = listOf<String>("Seoul", "Busan", "Incheon")
    
        fun getCities() : List<String> {
            return cities
        }
    
        fun getCity(name: String): String {
            return cities.get(cities.indexOf(name))
        }
    
        fun isExistsCity(name: String): Boolean {
            return cities.indexOf(name) > 0
        }
    }

    Repository에서 데이터를 가져와서 Activity에 전달하는 Domain Layer의 UseCase가 Activity에 전달합니다. 그래서 CityUseCase는 CityRepository를 참조해야 합니다.

    // CityUseCase.kt
    
    package com.hans.android.koinexam.domain.usecase
    
    import com.hans.android.koinexam.data.repository.CityRepository
    
    class CityUseCase(private val cityRepository: CityRepository) {
    
        fun getCities(): List<String> {
            return cityRepository.getCities()
        }
    
        fun getCity(name: String): String {
            return cityRepository.getCity(name)
        }
    
        fun isExistsCity(name: String): Boolean {
            return cityRepository.isExistsCity(name)
        }
    }

     

    3. Module 선언

    이제 코드를 작성했으니 Koin에 DI를 설정해줘야 합니다. 그래서 아래와 같이 Module을 선언합니다. 선언하는 방법은 너무 간단해서 부가 설명이 필요 없을 정도입니다.

    package com.hans.android.koinexam.di
    
    import com.hans.android.koinexam.data.repository.CityRepository
    import com.hans.android.koinexam.domain.usecase.CityUseCase
    import org.koin.dsl.module
    
    var myModule = module {
        single<CityRepository> { CityRepository() }
        single<CityUseCase> { CityUseCase(get()) }
    }

    module 안에 CityRepository와 CityUseCase를 선언한 것은 아주 간단합니다. 약간의 선언 방법을 설명드리자면

    • single<> : Singleton, 처음에 DI를 세팅할때 Singleton으로 생성해서 Inject로 사용시 하나의 객체를 사용합니다.

    • factory<> : Factory, Inject를 사용할때마다 새로운 객체를 생성합니다.

    • named() : Qualifier, 인터페이스로 DI를 만들고 주입은 Implemention으로 하는 방식을 선언할때 유용합니다.

      • ex) single<CityUseCaseImpl>(named("CityUseCase") { CityUserCaseImpl(get()) }

    CityUseCase는 CityRepository가 DI로 생성된 객체를 생성자 Parameter형태로 Inject을 받아서 사용하는 것입니다. 그래서 Module에 CityUseCase가 위와 같이 선언된 것입니다. get()에 아무 파라미터가 없는 이유는 CityUseCase에서 CityRepository라고 명시했기 때문에 클래스의 이름을 보고 유추해서 Inject합니다. Qualifier가 있다면 get("name") 형태로 기재해야 합니다.

     

    마지막으로 Application에서 Koin을 Loading합니다.

    // MyApplication.kt
    
    package com.hans.android.koinexam
    
    import android.app.Application
    import com.hans.android.koinexam.di.myModule
    import org.koin.android.ext.koin.androidContext
    import org.koin.android.ext.koin.androidLogger
    import org.koin.core.context.startKoin
    import org.koin.core.logger.Level
    
    class MyApplication: Application() {
    
        override fun onCreate() {
            super.onCreate()
    
            // start Koin
            startKoin {
                androidLogger(Level.DEBUG)
                androidContext(this@MyApplication)
                modules(myModule)
            }
        }
    }
    • androidLogger() : Koin이 로그를 남기는 레벨을 지정합니다.

    • androidContext() : 안드로이드에서 구동하기 위해서 Context를 지정합니다.

    • modules() : 선언한 Module을 지정합니다. 여러개의 모듈이 있는 경우 다음과 같이  지원합니다.

      • module(listOf(module1, module2, module3))

      • module(module1, module2, module3)

     

    4. DI 사용

    이제 Activity에서 어떻게 CityUseCase를 Inject하는지 알아보겠습니다.

    // MainActivity.kt
    
    package com.hans.android.koinexam.presenter
    
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.view.View
    import com.hans.android.koinexam.R
    import com.hans.android.koinexam.domain.usecase.CityUseCase
    import kotlinx.android.synthetic.main.activity_main.*
    import org.koin.android.ext.android.inject
    
    class MainActivity : AppCompatActivity() {
    
        private val cityUseCase: CityUseCase by inject()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    
        fun getCity(view: View) {
            val city = cityUseCase.getCities()
            cityResult.setText(city.toString())
        }
    }
    

     

    'private val cityUserCase: CityUseCase by inject()' 이걸로 끝입니다. Ultra Simple! (Dagger는 이제 안녕~)

     

    위의 모든 코드는 아래의 Github에서 확인하실 수 있습니다.

    https://github.com/goldfing/AndroidProjects/tree/master/KoinExam

     

    끝.

Designed by Tistory.