Darwin Compose with Decompose, MviKotlin, Koin

이태훈·2022년 6월 18일
0

Darwin Compose

목록 보기
2/4

안녕하세요 이번에는 Decompose에서 Darwin Compose를 구현해보겠습니다.

먼저 사용한 라이브러리 버전들은 다음과 같습니다.

  • AGP : 7.2.1
  • Kotlin : 1.6.21
  • Jetbrains Compose : 1.2.0-alpha01-dev675
  • Decompose : 0.6.0-native-compose-01

Decompose의 release note에 따라 버전을 맞춰줍니다.

Gradle 구성

먼저 gradle을 구성해보겠습니다.

Project Root Gradle

// build.gradle.kts
plugins {
    id("com.android.application") version "7.2.1" apply false
    id("com.android.library") version "7.2.1" apply false
    id("org.jetbrains.kotlin.android") version "1.6.21" apply false
    kotlin("plugin.serialization") version "1.6.21" apply false
    id("org.jetbrains.compose") version "1.2.0-alpha01-dev675" apply false
}

tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
}

// settings.gradle.kts
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }
}

enableFeaturePreview("VERSION_CATALOGS")

dependencyResolutionManagement {
    versionCatalogs {
        create("deps") {
            from(files("deps.version.toml"))
        }
    }
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }
}

rootProject.name = "Decompose_Sample"
include(":androidApp")
include(":shared")
include(":ui-compose")
include(":darwin-compose")

별다른 특이사항은 없으니 넘어가겠습니다.

저는 version catalog를 사용했기 때문에 settings.gradle에 version catalog를 사용하기 위한 코드를 작성해주었습니다.

ui-compose gradle

plugins {
	kotlin("multiplatform")
	id("com.android.library")
	id("org.jetbrains.compose")
}

kotlin {
	ios()
	android()

	sourceSets {
		commonMain {
			dependencies {
				implementation(project(mapOf("path" to ":shared")))
				implementation(compose.material)
				implementation(compose.foundation)
				implementation(deps.decompose.extension.compose)
			}
		}
	}
}

android {
	compileSdk = 31
	sourceSets["main"].manifest.srcFile("src/main/AndroidManifest.xml")
}

ui-compose 모듈의 gradle도 특이사항은 없습니다.

안드로이드에서도 ui 코드를 공유하기 위해 android library로 모듈을 구성해주고 configuration을 작성해줬습니다.

shared

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    kotlin("plugin.serialization")
    id("com.android.library")
    id("kotlin-parcelize")
}

version = "1.0"

kotlin {
    android()
    
	...
    
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(deps.kotlinx.coroutines)
                implementation(deps.kotlinx.serialization.json)
                api(deps.decompose.decompose)
                implementation(deps.decompose.extension.compose)
                implementation(deps.koin.core)
                implementation(deps.bundles.ktor)
                implementation(deps.bundles.mviKotlin)
            }
        }
        val commonTest by getting
        val androidMain by getting {
            dependencies {
                implementation(deps.bundles.compose)
                implementation(deps.ktor.okHttp)
            }
        }
        ...
    }
}

android {
    compileSdk = 31
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdk = 21
        targetSdk = 31
    }
}
	...
}

중간에 생략된 코드들이 조금 있는데, 이 부분은 따른 모듈을 구성하는 코드라 생략했습니다.

마찬가지로 별 무리 없이 이해할 수 있는 코드들이라 넘어가겠습니다.

Darwin compose

plugins {
	kotlin("multiplatform")
	id("org.jetbrains.compose")
}

kotlin {
	iosX64("uikitX64") {
		binaries {
			executable {
				entryPoint = "com.example.decomposesample.main"
				freeCompilerArgs += listOf(
					"-linker-option", "-framework", "-linker-option", "Metal",
					"-linker-option", "-framework", "-linker-option", "CoreText",
					"-linker-option", "-framework", "-linker-option", "CoreGraphics"
				)
			}
		}
	}

	sourceSets {
		commonMain {
			dependencies {
				implementation(project(mapOf("path" to ":shared")))
				implementation(project(mapOf("path" to ":ui-compose")))
				implementation(compose.material)
				implementation(compose.foundation)
			}
		}
	}
}

kotlin.targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
	binaries.all {
		binaryOptions["memoryModel"] = "experimental"
		binaryOptions["freezing"] = "disabled"
	}
}

compose.experimental {
	uikit.application {
		bundleIdPrefix = "com.example"
		projectName = "DecomposeSample"

		deployConfigurations {
			simulator("IPhone12Pro") {
				device = org.jetbrains.compose.experimental.dsl.IOSDevices.IPHONE_12_PRO
			}
		}
	}
}

kotlin {
	targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
		binaries.all {
			freeCompilerArgs += "-Xdisable-phases=VerifyBitcode"
		}
	}
}

Darwin Compose 모듈의 build gradle입니다.

낯선 코드들 부터 차근차근 보겠습니다.

kotlin.targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
	binaries.all {
		binaryOptions["memoryModel"] = "experimental"
		binaryOptions["freezing"] = "disabled"
	}
}

이 부분은 Kotlin MultiPlatform New Memoey Management의 가이드에 따라 작성해주었습니다.

저는 corotines 1.6.2 버전 및 ktor 2.0.2 버전을 사용했기 때문에 해당 스크립트를 작성했습니다.

compose.experimental {
	uikit.application {
		bundleIdPrefix = "com.example"
		projectName = "DecomposeSample"

		deployConfigurations {
			simulator("IPhone12Pro") {
				device = org.jetbrains.compose.experimental.dsl.IOSDevices.IPHONE_12_PRO
			}
		}
	}
}

IPhone 시뮬레이터에 앱을 빌드시키기 위해 추가했습니다.

kotlin {
	targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
		binaries.all {
			freeCompilerArgs += "-Xdisable-phases=VerifyBitcode"
		}
	}
}

공식 샘플을 보면
the current compose binary surprises LLVM, so disable checks for now.

위와 같이 첨언이 돼있습니다.
kotlin native가 llvm을 거쳐 실행 파일을 만들어 주는데, 이 과정에서 현재 jetbrains compose가 충돌이 일어나는가 싶습니다. 업데이트 되면서 해결이 될 것 같습니다.

구현부

먼저 buisness logic을 공유할 shared 모듈부터 구현해보겠습니다.

Domain Layer와 Data Layer, Presentation Layer 이전에 포스팅한 글을 참고해주시면 되겠습니다.

ui-compose

안드로이드 모듈에서 Jetpack Compose를 이용하여 뷰를 짜듯이 짜주시면 됩니다.

@Composable
fun RootContent(root: TmdbRoot) {
	Children(routerState = root.routerState) {
		when(val child = it.instance) {
			is TmdbRoot.Child.Main -> MainContent(child.component)
		}
	}
}

@Composable
fun MainContent(main: TmdbMain) {
	val model by main.model.subscribeAsState()

	when (val result = model.movies) {
		is Result.Success -> Text(text = "Result is Success")
	}
}

darwin compose

fun main() {
	val args = emptyArray<String>()
	memScoped {
		val argc = args.size + 1
		val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues()
		autoreleasepool {
			UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate))
		}
	}
}

class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol {
	companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta

	@ObjCObjectBase.OverrideInit
	constructor() : super()

	init {
		startKoin()
	}

	private val lifecycle = LifecycleRegistry()

	private val root = TmdbRootComponent(componentContext = DefaultComponentContext(lifecycle = lifecycle))

	private var _window: UIWindow? = null
	override fun window() = _window
	override fun setWindow(window: UIWindow?) {
		_window = window
	}

	override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map<Any?, *>?): Boolean {
		window = UIWindow(frame = UIScreen.mainScreen.bounds)
		window!!.rootViewController = Application("Minesweeper") {
			Column {
				Spacer(modifier = Modifier.height(48.dp))
				RootContent(root)
			}
		}
		window!!.makeKeyAndVisible()
		return true
	}

	override fun applicationDidBecomeActive(application: UIApplication) {
		lifecycle.resume()
	}

	override fun applicationWillResignActive(application: UIApplication) {
		lifecycle.stop()
	}

	override fun applicationWillTerminate(application: UIApplication) {
		lifecycle.destroy()
	}
}

Decompose를 이용하지 않았을 때와 큰 차이가 없습니다.

마찬가지로 공통 Jetbrains Compose 모듈에서 컴포저블 함수를 가져와 skiko engine에서 그려줍니다.

먼저, koin의 모듈을 설정해줍니다.

init {
	startKoin()
}

Decompose의 라이프 사이클을 맞추기 위해 아래와 같은 함수도 추가해줍니다.

private val lifecycle = LifecycleRegistry()

override fun applicationDidBecomeActive(application: UIApplication) {
		lifecycle.resume()
	}
override fun applicationWillResignActive(application: UIApplication) {
		lifecycle.stop()
	}
override fun applicationWillTerminate(application: UIApplication) {
		lifecycle.destroy()
	}

android app

안드로이드에서는 예전과 같습니다.

코인 설정해주고 액티비티에서 뷰를 그려주면 됩니다.

class Application : Application() {

	override fun onCreate() {
		super.onCreate()

		startKoin {
			androidContext(this@Application)
			modules(repositoryModule, interactorModule, networkModule, storeModule)
		}
	}
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val tmdbRoot = TmdbRootComponent(defaultComponentContext())
        setContent {
            RootContent(root = tmdbRoot)
        }
    }
}
profile
https://www.linkedin.com/in/%ED%83%9C%ED%9B%88-%EC%9D%B4-7b9563237

0개의 댓글