Compose MultiplatformでKoinを使って環境別のDIを行う方法
こんにちは!sugitaniと申します。ブラックキャット・カーニバル(略称ブラキャニ)というCompose Multiplatformで作られたSNSアプリを開発しています。
本稿はブラキャニの開発で得られた"あれどうやるんだっけ" を備忘録も兼ねて共有していくシリーズの3作目です。
Koinを使って環境別のDIを行う
Koinは元々はAndroid向けのDI支援フレームワークでしたが、現在はKMMにも対応しています。
Koinを使い、以下の四種のDIを行う方法をご紹介します
-
commonMain
に作ったクラスをDIする - Android向けのDIする
- iOS向けでKotlinで実装したクラスをDIする
- iOS向けでSwiftで実装したクラスをDIする
ライブラリを追加する
gradle/libs.versions.toml
に追加します
[versions]
// …
koin = "4.0.4"
[libraries]
// …
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android" }
koin-compose = { module = "io.insert-koin:koin-compose" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-test = { module = "io.insert-koin:koin-test" }
composeApp/build.gradle.kts
に追加します
// …
kotlin {
// …
sourceSets {
commonMain.dependencies {
// …
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.compose)
implementation(libs.koin.core)
implementation(libs.koin.test)
}
androidMain.dependencies {
// …
implementation(libs.koin.android)
}
}
}
DIしたいクラスを定義&実装する
以下の定義を各プラットフォーム毎に実装していくとします
composeApp/src/commonMain/kotlin/cc/bcc/cmpexamples/example003/ExampleClass.kt
package cc.bcc.cmpexamples.example003
interface CommonExampleClass {
fun hello(): String
}
interface PlatformExampleClass {
fun hello(): String
}
まずはCommonExampleClassを実装します
composeApp/src/commonMain/kotlin/cc/bcc/cmpexamples/example003/CommonExampleClassImpl.kt
package cc.bcc.cmpexamples.example003
class CommonExampleClassImpl : CommonExampleClass {
override fun hello(): String = "Hello from CommonExampleClassImpl"
}
次にAndroid実装です
composeApp/src/androidMain/kotlin/cc/bcc/cmpexamples/example003/AndroidExampleClassImpl.kt
package cc.bcc.cmpexamples.example003
class AndroidExampleClassImpl : PlatformExampleClass {
override fun hello(): String = "Hello from AndroidExampleClassImpl"
}
次にiOS > KMM実装です
composeApp/src/iosMain/kotlin/IosKotlinExampleClass.kt
import cc.bcc.cmpexamples.example003.PlatformExampleClass
class IosKotlinExampleClass : PlatformExampleClass {
override fun hello(): String = "Hello from IosKotlinExampleClass"
}
最期に iOS > Swift実装です
iosApp/iosApp/IosSwiftExampleClass.swift
import ComposeApp
import Foundation
class IosSwiftExampleClass: PlatformExampleClass {
func hello() -> String {
return "Hello from IosSwiftExampleClass"
}
}
Koinの準備 > 共通編
まず共通で使えるモジュール群を使う準備をします
package cc.bcc.cmpexamples.example003
import org.koin.core.KoinApplication
import org.koin.core.module.Module
import org.koin.dsl.module
val commonModules: KoinApplication.() -> Module = {
module {
single<CommonExampleClass> {
CommonExampleClassImpl()
}
}
}
Koinの準備 > Android編
Androidでは以下のようにKoinを開始します
composeApp/src/androidMain/kotlin/cc/bcc/cmpexamples/example003/Application.kt
package cc.bcc.cmpexamples.example003
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module
class Application : android.app.Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@Application)
modules(
commonModules(),
module {
single<PlatformExampleClass> { AndroidExampleClassImpl() }
},
)
}
}
}
AndroidManifest.xml
にApplicationが使われるようにandroid:nameを追加することを忘れないでください
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".Application"
android:icon="@mipmap/ic_launcher"
android:label="BccExamples003"
android:theme="@android:style/Theme.Material.NoActionBar">
<!-- いろいろ -->
</application>
</manifest>
Koinの準備 > iOS編
Swift側からKoinを呼べるようにヘルパー関数を用意します
composeApp/src/iosMain/kotlin/Helper.kt
@file:Suppress("unused")
import cc.bcc.cmpexamples.example003.PlatformExampleClass
import cc.bcc.cmpexamples.example003.commonModules
import org.koin.core.context.startKoin
import org.koin.dsl.module
// KMMだけで済ます場合
fun initKoinType1() {
startKoin {
modules(
commonModules(),
module {
single<PlatformExampleClass> { IosKotlinExampleClass() }
},
)
}
}
// SwiftからもInjectする場合
fun initKoinType2(exampleClassFactory: () -> PlatformExampleClass) {
startKoin {
modules(
commonModules(),
module {
single { exampleClassFactory() }
},
)
}
}
Swift側から以下のように利用します
import ComposeApp
import SwiftUI
@main
struct ComposeApp: App {
init() {
HelperKt.doInitKoinType1()
// ↑か↓かどっちかやりたい方で
// HelperKt.doInitKoinType2 {
// IosSwiftExampleClass()
// }
}
var body: some Scene {
WindowGroup {
ContentView().ignoresSafeArea(.all)
}
}
}
// …
使い方
以下のように使います。
composeApp/src/commonMain/kotlin/cc/bcc/cmpexamples/example003/App.kt
package cc.bcc.cmpexamples.example003
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.getKoin
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
internal fun App() {
val logs = mutableStateListOf<String>()
val koin = getKoin()
LaunchedEffect(Unit) {
val common = koin.get<CommonExampleClass>()
val platform = koin.get<PlatformExampleClass>()
logs.add(common.hello())
logs.add(platform.hello())
}
MaterialTheme {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = { Text("Koin Example") },
)
},
) { innerPadding ->
Column(
modifier =
Modifier
.padding(innerPadding)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
logs.forEach { log ->
Text(text = log, modifier = Modifier.padding(vertical = 4.dp))
}
}
}
}
}
サンプルプロジェクト
本稿のソースコード、および動作するコードは
にあります。免責事項
このコードはあくまで"自分はこう実装した"という例ですので、よりよい方法がある可能性があります。見つけたら教えてください!
Discussion