🍿

Compose Multiplatformでexpect/actialでContextをうまく扱う方法

に公開

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

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

expect/actialでContextをうまく扱う方法

以下の2つを使い分けています

  1. @Composableなビルダー関数を作ってどうにかする
  2. Coil3同梱のPlatformContextでどうにかする

原則として1を使いますが、1では面倒な場面では2を使います。

@Composableなビルダー関数を作ってどうにかする

Contextを使う例題としてGoogleを開く処理を行ってみます。

commonMainに以下を作成します

interface GoogleOpenerType1 {
    fun openGoogle()
}

@Composable
expect fun rememberGoogleOpenerType1(): GoogleOpenerType1

Android以下に実装を作成します

@Composable
actual fun rememberGoogleOpenerType1(): GoogleOpenerType1 {
    val context = LocalContext.current

    val opener =
        remember(context) {
            object : GoogleOpenerType1 {
                override fun openGoogle() {
                    val intent =
                        android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
                            data = "https://www.google.com".toUri()
                        }
                    context.startActivity(intent)
                }
            }
        }

    return opener
}

iOS以下に実装を作成します

@Composable
actual fun rememberGoogleOpenerType1(): GoogleOpenerType1 =
    remember {
        object : GoogleOpenerType1 {
            override fun openGoogle() {
                val url = NSURL(string = "https://www.google.com")
                UIApplication.sharedApplication.openURL(url, emptyMap<Any?, Any>()) {}
            }
        }
    }

②Coil3同梱のPlatformContextでどうにかする

画像表示ライブラリCoil3同梱のPlatformContextはAndroidから利用する時はただのContextに、iOSから利用するときは空の実装になります。

どのみちCoilは大体の場合で使うと思うので①のアプローチだと面倒な場合は活用しましょう

commonMainに以下を作成します

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect class GoogleOpenerType2(
    context: PlatformContext,
) {
    fun openGoogle()
}

Android以下に実装を作成します

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class GoogleOpenerType2 actual constructor(
    private val context: PlatformContext,
) {
    actual fun openGoogle() {
        val intent =
            android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
                data = "https://www.google.com".toUri()
            }
        context.startActivity(intent)
    }
}

iOS以下に実装を作成します

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class GoogleOpenerType2 actual constructor(
    context: PlatformContext,
) {
    actual fun openGoogle() {
        val url = NSURL(string = "https://www.google.com")
        UIApplication.sharedApplication.openURL(url, emptyMap<Any?, Any>()) {}
    }
}

作成したType1,Type2は以下のように使います

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
internal fun App() {
    val type1 = rememberGoogleOpenerType1()

    val context = LocalPlatformContext.current
    val type2 = remember(context) { GoogleOpenerType2(context) }

    MaterialTheme {
        Scaffold(
            topBar = {
                CenterAlignedTopAppBar(
                    title = { Text("Use Context Example") },
                )
            },
        ) { innerPadding ->
            Column(
                modifier = Modifier.padding(innerPadding).fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Button(onClick = { type1.openGoogle() }) {
                    Text("Open Google(type1)")
                }

                Spacer(modifier = Modifier.height(16.dp))

                Button(onClick = { type2.openGoogle() }) {
                    Text("Open Google(type2)")
                }
            }
        }
    }
}

サンプルプロジェクト

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

免責事項

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

以下宣伝

ブラックキャット・カーニバル

Discussion