기본적인 Koin 사용법은 앞 포스팅에서 다뤘다. 이제 안드로이드에서 사용해보자. 버전카탈로그를 사용한다고 가정한다. 우선 libs.versions.toml 에 다음과 같이 추가한다. bom을 사용하기 때문에, 그 이후에는 version.ref런스 명시가 필요없다.
[versions]
...
koin-bom = "4.0.0"
...
[libraries]
...
koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin-bom"}
koin-core = { group = "io.insert-koin", name = "koin-core"}
koin-android = { group = "io.insert-koin", name = "koin-android"}
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose"}
koin-androidx-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation"}
koin-junit4 = { group = "io.insert-koin", name = "koin-test-junit4"}
koin-core, koin-android외 라이브러리들은 필요한 것들을 찾아서 추가한다고 생각하면 된다. 이제, build.gradle.kts 파일에 해당 라이브러리들을 추가해보자.
dependencies {
...
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.compose.navigation)
implementation(libs.koin.junit4)
...
}
Koin Usage : https://insert-koin.io/docs/quickstart/android-compose
안드로이드에 DI를 사용하는 가장 큰 이유중 하나는 MVVM구조에서 Repository, Viewmodel들을 관리하기 위함일 것이다. 이전 포스트에서 다룬 Kotlin에서 이미 동일한 구조로 Repository를 만들었었으므로, 동일한 형태로 만들 거다. User와 Repository를 추가해주자.
data class User(val name : String)
interface UserRepository {
fun findUser(name : String): User?
fun addUsers(users : List<User>)
}
class UserRepositoryImpl : UserRepository {
private val _users = arrayListOf<User>()
override fun findUser(name: String): User? {
return _users.firstOrNull { it.name == name }
}
override fun addUsers(users : List<User>) {
_users.addAll(users)
}
}
이제 동일하게 Koin module을 정의해보자.
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
}
Koin module의 정의만으론 Koin을 사용할 수 없다. 앞에서와 같이 startKoin()을 불러줘야 하는데, 안드로이드니까 Application 클래스를 상속받아 새로 만들고, 거기서 startKoin을 불러준다.
// MainApplication.kt file
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
private val userRepository : UserRepository by inject()
startKoin {
androidLogger()
androidContext(this@MainApplication)
modules(appModule)
}
userRepository.addUsers(DEFAULT_USERS)
}
}
------------------------------------------------
// AndroidManifest.xml file
...
<application
android:name=".MainApplication"
...
위 코드를 보면, userRepository를 by inject로 부르고나서 startKoin을 호출하고 userRepository.addUsers(DEFAULT_USERS) 로 테스트할 기본값을 넣어주고 있다. 이것은 온전히 테스트를 위한 것으로, 사실은 여기서 처리될 코드가 아니다. 한가지 더 짚고 넘어가자면, startKoin호출 전에 injection을 시키는 것처럼 보여 문제가 되지 않나 생각되는데, by inject()는 lazy injection이기 때문에 실제로는 그 뒤에 addUsers()를 호출할 때 실행된다.
마지막으로 알고 있겠지만, 안드로이드에서 Application을 사용자가 정의할 경우, AndroidManifest.xml에도 추가해줘야 한다. 위 코드의 아랫 부분을 참조하자. 코틀린 때와 다른점은 androidContext()를 이용해서 Application Context를 넘겨주고 있다는 점이다.
UserViewModel 클래스를 다음과 같이 만들자.
class UserViewModel(private val repository: UserRepository) : ViewModel() {
fun sayHello(name : String) : String{
val foundUser = repository.findUser(name)
return foundUser?.let { "Hello '$it' from $this" } ?: "User '$name' not found!"
}
}
이제 이 UserViewModel도 Koin module에 추가해준다. viewmodel은 안드로이드에만 있는 특별한 것으로, viewModelOf()를 사용해준다. hilt에서도 viewmodel은 별도의 annotation이 사용되는 것처럼, 이는 안드로이드의 lifecycle을 고려한 것으로 알고 있다.
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
viewModelOf(::UserViewModel)
// viewModel { MyViewModel(get()) }
}
위 코드에서 주석처리한 부분을 보면, get()을 이용해서 인자인 UserRepository를 넘겨주는 걸 볼 수 있다. 그러나, 보다 간단히 dsl형태로 위와같이 사용이 가능하다.
앞에서 MainApplication내에서 UserRepository를 injection시키고 있지만, 이건 테스트를 위한 코드 부분으로 실제로는 Injection을 명시적으로 안하게 될거다. 대신에 UserRepository는 UserViewModel에서 get()으로 얻어와 사용하기 때문에, 이 시점에 injection이 일어날 것이고, 우리는 UserViewModel만 inject 시키면 된다.
Android Compose에서 ViewModel이 사용되는 부분은 UI Composable 함수들이다. 다음과 같이 ViewModelInject() composable function을 추가해보자.
@Composable
fun ViewModelInject(userName : String, viewModel: UserViewModel = koinViewModel()){
Text(text = viewModel.sayHello(userName), modifier = Modifier.padding(8.dp))
}
코드의 내용을 보면, viewmodel injection을 위해 koinViewModel()함수가 사용되고 있다. 그리고, viewmodel의 sayHello()를 불러주고 있다. 앞의 UserViewModel을 참조하면, sayHello()는 User 정보를 가지고 문자열을 돌려주는 함수이다.
공식 사이트의 예제를 보면, Viewmodel외에 StateHolder에 대한 예제도 보여준다. StateHolder란, 안드로이드 Compose의 state를 별도의 일반 클래스를 만들어 저장하고, 위임하는 방식이다. Viewmodel도 일종의 Stateholder로 생각할 수 있다. 문제는, Koin의 예제에서 이 stateholder를 factoryOf()를 이용하여 모듈에서 정의한다는 점이다.
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
factoryOf(::UserStateHolder)
viewModelOf(::UserViewModel)
}
이게 과연 유효한 방식인지 좀 의문이 있다. factoryOf()를 사용하면, injection될 때마다 새로운 인스턴스가 생성이 되는데, recompose 될 때마다 새로운 인스턴스가 생성되는 것 아닌가 싶은 부분. compose의 경험치가 적어서 이부분을 잘 모르겠다. 예제의 stateholder도 stateholder의 사용예로 적절하지 않아보여서 일단, 안쓰는걸로.
다시 viewmodel로 돌아가자면, 마지막으로 Activity에서 ViewModelInject() composable 함수를 사용하는 일만 남았다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
KoinAndroidContext {
App()
}
}
}
}
}
@Composable
fun App(){
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center){
val userName = DefaultData.DEFAULT_USER.name
ViewModelInject(userName)
}
}
이렇게해서 안드로이드에서의 Koin 사용을 알아봤다. 이걸로 충분하지 않을건데, 기본을 뚫어놨으니 공식문서에서 필요한 부분을 찾아보면 될 거 같다. 물론 검증은 필수.