요즘 Compose Multiplatform에 대한 관심도가 많이 높아진 것 같습니다.
Jetpack ViewModel은 기존에 Android에서만 사용이 가능했는데, 최근 KMP에서도 사용 가능하도록 업데이트를 하기도 했습니다.
이 외에 Navigation Component, Room 등도 이러한 양상을 보이고, 많은 라이브러리들이 KMP 환경을 제공하기 위해 노력하고 있는 듯 합니다.
https://www.jetbrains.com/ko-kr/compose-multiplatform/
저 또한 CMP에 관심을 가지고 있는데, 지금까지 잘 사용을 하지 않은 이유는 적합한 아키텍처 또는 아키텍처 구성요소를 선택하지 못했기 때문입니다.
여러가지 유용한 도구들이 있었지만 딱히 호감이 가지 않았던 탓도 있습니다.
최근 Circuit에 대해 알아보면서 기존의 ViewModel 기반의 MVVM, MVI와 다른 접근법에 관심을 가지게 되었고, CMP에 적용이 가능하기 때문에 사이드프로젝트에서 활용해보기 위해 스터디를 하고 있습니다.
오늘의 포스팅은 Android, iOS 환경을 위한 CMP 프로젝트에 Circuit 환경설정에 대해 다룹니다.
글을 읽기 귀찮으신 분들을 위해 Github에 Template 저장소로 생성해 두었으니 코드를 그대로 사용하셔도 됩니다.
https://github.com/duitddu/template-cmp-circuit
프로젝트 생성
KMP 프로젝트의 생성은 KMP Wizard 도구를 사용하면 굉장히 편리합니다.
https://kmp.jetbrains.com/
프로젝트 명, 원하는 플랫폼등을 지정하면 프로젝트를 뚝딱 만들어줍니다.
이번 포스팅은 Android, iOS만을 목표로 합니다.
의존성 추가
포스팅 제목에 부합할 수 있도록 Circuit과 Koin의 의존성을 추가해야 합니다.
Koin은 코틀린환경에서 사용할 수 있는 의존성 프레임워크입니다.
Hilt는 어노테이션을 통해 코드를 생성해주는 반면, Koin은 직접 코드를 작성해서 의존성 코드를 작성해주어야 합니다.
장점과 단점이 분명하지만, KMP 환경에서 손쉽게 사용할 수 있으므로 선택했습니다.
Circuit, Koin에 관련해서는 공식문서를 참고해보면 좋습니다.
https://slackhq.github.io/circuit/
이제 프로젝트에 의존성을 추가해줍시다.
// libs.versions.toml
[versions]
...
koin = "4.0.1"
circuit = "0.25.0"
[libraries]
...
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
circuit-runtime = { module = "com.slack.circuit:circuit-runtime", version.ref = "circuit" }
circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version.ref = "circuit" }
circuit-codegen-annotation = { module = "com.slack.circuit:circuit-codegen-annotations", version.ref = "circuit" }
circuit-codegen-ksp = { module = "com.slack.circuit:circuit-codegen", version.ref = "circuit" }
circuitx-android = { module = "com.slack.circuit:circuitx-android", version.ref = "circuit" }
[plugins]
...
[bundles]
circuit = [
"circuit-foundation",
"circuitx-android",
"circuit-runtime"
]
// composeApp/build.gradle.kts
...
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
...
api(libs.koin.core)
implementation(libs.koin.compose.viewmodel)
implementation(libs.bundles.circuit)
}
androidMain.dependencies {
...
implementation(libs.koin.android)
implementation(libs.koin.compose)
}
...
}
}
...
의존성 추가는 간단히 아래 두 스텝이면 완료입니다.
1. libs.versions.toml에 버전, 라이브러리, 플러그인 추가
2. build.gradle.kts의 commonMain, androidMain, iosMain 에 의존성 추가
모듈 정의
의존성 주입 도구로 Koin을 선택했으므로 모듈들을 생성해줍시다.
첫번째로, Circuit의 Presenter를 주입할때 사용하기 위한 Presenter Factory 모듈을 아래와 같이 작성합니다.
// Circuit Presenter Factory
class CircuitPresenterFactory<P : Presenter<*>>(
private val factory: (Navigator, Screen) -> P
) : Presenter.Factory {
override fun create(
screen: Screen,
navigator: Navigator,
context: CircuitContext
): Presenter<*> {
return factory(navigator, screen)
}
}
fun Module.circuitPresenterFactory(presenter: Scope.(Navigator, Screen) -> Presenter<*>) {
factory<Presenter.Factory> {
CircuitPresenterFactory { navigator, screen ->
presenter(navigator, screen)
}
}
}
두번째로, Circuit의 UI Factory를 주입할때 사용하기 위한 UI Factory 모듈을 아래와 같이 작성합니다.
class CircuitUiFactory(
private val factory: @Composable (CircuitUiState, Modifier) -> Unit
) : Ui.Factory {
override fun create(screen: Screen, context: CircuitContext): Ui<*>? {
return ui<CircuitUiState> { state, modifier ->
factory(state, modifier)
}
}
}
fun Module.circuitUiFactory(ui: @Composable (CircuitUiState, Modifier) -> Unit) {
factory<Ui.Factory> {
CircuitUiFactory { state, modifier ->
ui(state, modifier)
}
}
}
세번째로, 앞서 만든 Factory 모듈로 Circuit 을 초기화하여 주입하기 위한 모듈을 아래와 같이 작성합니다.
val circuitModule = module {
circuitPresenterFactory { navigator, screen ->
when (screen) {
is MainScreen -> MainPresenter(screen, navigator)
else -> throw Exception("Invalid Screen Detected! :: $screen")
}
}
circuitUiFactory { state, modifier ->
when (state) {
is MainScreen.State -> Main(state, modifier)
}
}
single {
Circuit.Builder()
.addUiFactories(getAll())
.addPresenterFactories(getAll())
.build()
}
}
Circuit의 State, Screen, Presenter가 추가되면 이 모듈에서 생성할 수 있도록 코드를 추가해주어야 합니다.
Hilt나 Kotlin-Inject와 같이 어노테이션 기반으로 코드를 생성하는 의존성 주입 프레임워크는 이것이 필요없지만, Koin은 일일이 추가해주어야 하는 단점이 여기서 발생합니다.
네번째로, Koin 을 설정하여 의존성 주입을 시작하는 함수를 아래와 같이 하나 작성해줍니다.
fun initKoin(config: KoinAppDeclaration? = null) {
startKoin {
config?.invoke(this)
modules(circuitModule)
}
}
Circuit 설정
이제 Android, iOS 영역에서 Koin을 초기화하도록 코드를 추가하고 Common 영역에서 Circuit을 사용해 UI를 구성하면 됩니다.
첫번째로, Android 영역 코드를 아래와 같이 작성합니다.
// androidMain > Application, MainActivity
class Application : Application() {
override fun onCreate() {
super.onCreate()
initKoin {
androidContext(this@Application)
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val koin = getKoin()
val circuit: Circuit = koin.get()
App(circuit)
}
}
}
두번째로, iOS 영역 코드를 아래와 같이 작성합니다.
// iosMain > MainViewController
fun MainViewController() = ComposeUIViewController(
configure = {
initKoin()
}
) {
val koin = getKoin()
val circuit: Circuit = koin.get()
App(circuit)
}
세번째로, Common 영역 코드를 아래와 같이 작성합니다.
// commonMain > App
@Composable
@Preview
fun App(circuit: Circuit) {
AppTheme {
val backStack = rememberSaveableBackStack(root = MainScreen)
val navigator = rememberCircuitNavigator(backStack) {}
CircuitCompositionLocals(circuit) {
NavigableCircuitContent(navigator = navigator, backStack = backStack)
}
}
}
마치며
이제 Circuit, Koin으로 CMP 프로젝트를 개발할 준비가 완료되었습니다.
설정 자체가 복잡할 것이 없어 손쉽게 구성할 수 있었습니다.
아직 막 시작한 단계라 어떤 개발경험을 가지게 될진 모르겠지만, 긍정적인 경험이 될 것이라 기대하고 있습니다.
Android, iOS를 동시에 개발해야 하는 니즈가 있다면 CMP + Circuit 조합을 고려해보면 좋을 것 같습니다.
'개발 > Android' 카테고리의 다른 글
[Android] Retrofit Call Adapter를 활용해서 효과적인 에러 핸들링 하기 (0) | 2025.01.24 |
---|---|
[Android] APK 파헤치기 (0) | 2025.01.05 |
[Android] Jetpack Compose - UI Test. 요약본 리뷰 (0) | 2024.12.09 |
[Android] Circuit + Compose로 Pokedex를 구현해보자. (feat. MVI) (2) | 2024.12.01 |
[Android/안드로이드] Jetpack Compose - Stability와 Recomposition 그리고 최적화 (0) | 2024.06.27 |