KC Blog

Compose Multiplatform 実践:expect と actual を使用したクロスプラットフォームコードの実装

5 min read
CrossPlatform#CMP#Kotlin

はじめに

Compose Multiplatform (略称CMP)

コードを共有することで

重複作業を減らすだけでなく

開発効率とコードの一貫性を向上させることができます

CMPは開発者にクロスプラットフォームソリューションを提供し

同じビジネスロジックを異なるプラットフォーム上で実行できるようにします

この記事では

CMPexpectactualキーワードを使用してクロスプラットフォームコードを実装する方法を探り

実践的な経験を共有します

expect と actual とは?

まずは Compose MultiplatformKotlin Multiplatform について簡単に理解しましょう

  • expectキーワードは共有コードでプラットフォーム固有のインターフェースやクラスを宣言するために使用され

    actualキーワードは特定のプラットフォームでそのインターフェースやクラスを実装するために使用されます

簡単な例を見てみましょう

以下のコードは各プラットフォームの名前を返す方法を示しています

commonMainexpectキーワードを使用して関数を宣言し

CMPプロジェクトの他のプラットフォームでその関数を実装することを期待しています

// in ~/commonMain/.../xxx.kt
expect fun getPlatformName(): String

// in ~/androidMain/.../xxx.kt
actual fun getPlatformName(): String {
    return "Android"
}

// in ~/iosMain/.../xxx.kt
actual fun getPlatformName(): String {
    return "iOS"
}

また

expectactualは関数だけでなく

クラスにも使用できます

これにより、共有コード内で

プラットフォーム固有のロジックを柔軟に定義し

特定のプラットフォームで実装を提供できます

// in ~/commonMain/.../FileSystem.kt
expect class FileSystem {
    fun readFile(path: String): String
}

// in ~/androidMain/.../FileSystem.kt
actual class FileSystem {
    actual fun readFile(path: String): String {
        // Androidプラットフォーム固有のファイル読み込みロジック
    }
}

// in ~/iosMain/.../FileSystem.kt
actual class FileSystem {
    actual fun readFile(path: String): String {
        // iOSプラットフォーム固有のファイル読み込みロジック
    }
}
実際の例
  • 一昨日のmaterial 3テーマの設定時に

    expect function として setStatusBarStyle を設定しました

    これは主にクロスプラットフォーム関数を期待

    特定のプラットフォームでステータスバーを設定できるようにするためです

// in ~/commonMain/.../xxxx.kt
@Composable
expect fun setStatusBarStyle(
    backgroundColor: Color,
    isDarkTheme: Boolean
)

// in ~/androidMain/.../xxx.kt
@Composable
actual fun setStatusBarStyle(backgroundColor: Color, isDarkTheme: Boolean) {

    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = backgroundColor.toArgb()
            window.navigationBarColor = Color.Transparent.toArgb()
        }
    }
}

// in ~/iosMain/.../xxx.kt
@Composable
actual fun setStatusBarStyle(backgroundColor: Color, isDarkTheme: Boolean) {
    DisposableEffect(isDarkTheme) {
        val statusBarStyle = if (isDarkTheme) {
            UIStatusBarStyleLightContent
        } else {
            UIStatusBarStyleDarkContent
        }
        UIApplication.sharedApplication.setStatusBarStyle(statusBarStyle, animated = true)
        onDispose { }
    }
}
  • 2つ目の例は、特定のプラットフォームにkoinを注入する場合です

    そのプラットフォームが特定のinstanceを必要とする場合

    そのプラットフォームで実装する必要があるかもしれません

    この場合、commonMainにexpectのkoin moduleを作成できます

以下は例です(後ほどkoinの使用方法についてさらに詳しく説明します)

// in ~/commonMain/.../xxxx.kt
expect val platformModule: Module

fun appModule() =
    listOf(platformModule)

// in ~/androidMain/.../xxx.kt
actual val platformModule: Module = module {
    /** Datastore*/
    single { dataStore(get<Context>()) }

    /** Database*/
    single<RoomDatabase.Builder<AppDatabase>> {
        getAppDatabase(get())
    }
}

// in ~/iosMain/.../xxx.kt
actual val platformModule: Module = module {
    /** DataStore*/
    single { dataStore() }

    /** Database*/
    single { getAppDatabase() }
}

小技巧

IDEには

実装しているclassがどのプラットフォームにあるかを区別しやすくする方法があります

共有コードでexpectキーワードを記述する際

他のプラットフォームでまだ対応するactualが実装されていない場合

IDEは未実装であることを通知します

この時、自動生成をクリックするだけで

IDEは未実装のファイルを作成し

特定のプラットフォームのファイル名に対応する文字列を追加します

例えば、xxx.android.ktやxxx.ios.ktなどです。

これにより

各classが属するプラットフォームをより直感的に理解できます

例えば:

command+fで実装した関数を検索するとき

現在検索されているのがどのプラットフォームのclassなのかが明確にわかります。

https://ithelp.ithome.com.tw/upload/images/20240809/20168335MtnaW60N59.png