💉

Compose MultiplatformでKoinを使って環境別のDIを行う方法

に公開

こんにちは!sugitaniと申します。ブラックキャット・カーニバル(略称ブラキャニ)というCompose Multiplatformで作られたSNSアプリを開発しています。

本稿はブラキャニの開発で得られた"あれどうやるんだっけ" を備忘録も兼ねて共有していくシリーズの3作目です。

Koinを使って環境別のDIを行う

Koinは元々はAndroid向けのDI支援フレームワークでしたが、現在はKMMにも対応しています。

Koinを使い、以下の四種のDIを行う方法をご紹介します

  1. commonMainに作ったクラスをDIする
  2. Android向けのDIする
  3. iOS向けでKotlinで実装したクラスをDIする
  4. 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))
                }
            }
        }
    }
}

サンプルプロジェクト

本稿のソースコード、および動作するコードは
https://github.com/blackcat-carnival/cmp-examples/tree/main/003.di_by_koin
にあります。

免責事項

このコードはあくまで"自分はこう実装した"という例ですので、よりよい方法がある可能性があります。見つけたら教えてください!

Black Cat Carnival

Discussion