KC Blog

Compose Multiplatform 実践:CMPでComposeを使用したクロスプラットフォーム画面の実装

7 min read
CrossPlatform#CMP#Kotlin

はじめに

Compose Multiplatform (略称CMP)

昨日、共通のMaterial3 Themeを構築しました

今日はクロスプラットフォームアプリの画面を作成していきます

CMPではComposeを使用してAndroidとiOSの画面を作成し

Compose UIは完全にcommonMain内に実装します

つまりUI部分はすべて共有できます

さらに、AndroidでもUIフレームワークとしてComposeの使用を全面的に推進しているため

すでにComposeに慣れている人にとっては非常に有利です

最初のCompose画面を作成する

  • まずはCMPでComposeを使用して基本的なHello World画面を作成する方法を見てみましょう

(不変の例であるHello world XD)

Compose宣言的UIを採用しているため

実装する関数の前に@Composableを追加するだけで

Composeのコンポーネントになります

CMPのcommonMainに以下を追加します

// in ~/commonMain/

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name !")
}
  • プレビューを表示したい場合

    別の関数を作成し、その前に@Previewを追加するだけで

    IDE上でComposeのプレビューを表示できます

// in ~/commonMain/

@Preview
@Composable
fun GreetingPreview() { Greeting("Compose") }

実際にIDEの右側に@Previewの画面が表示されます

Cover

ComposeコンポーネントのModifier

ModifierはComposeでコンポーネントを修飾・設定するためのツールです

Compose UIコンポーネントの動作や外観を変更するための様々な機能を提供します

Modifierを入力して

展開してみると

様々なオプションが用意されており

UIの動作や外観を設定できます

例えば、backgroundcolor、align、height、width、onClick...など

非常に多くの機能があるので、興味があれば自分で確認してみてください:

Cover

  • 昨日Themeを作成した場合

    Material3 themeを使用してコンポーネントの背景色を設定できます

// in ~/commonMain/

@OptIn(KoinExperimentalAPI::class)
@Composable
@Preview
fun App() {
    //ElegantAccessComposeThemeを通じてMaterial 3テーマを設定
    ElegantAccessComposeTheme {
        Greeting("Compose")
    }
}

そして、このようにTextの外側にColumnを追加し

Modifier.background(color = MaterialTheme.colorScheme.background)を使用します

// in ~/commonMain/

@Composable
fun Greeting(name: String) {
    Column(
        modifier = Modifier
            .background(color = MaterialTheme.colorScheme.background)
    ) {
        Text(text = "Hello $name !")
    }
}
Composeトップバーの作成
  • アプリ開発において、AndroidでもiOSでも

    カスタムツールバーが必要になることがよくあります

    Cover

  • そこで再利用可能なトップバーを作成できます

//in ~/commonMain/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainAppBar(
    modifier: Modifier = Modifier,
    config: MainAppBarConfig,
    elevation: Dp = 4.dp,
    containerColor: Color = MaterialTheme.colorScheme.primaryContainer
) {
    CenterAlignedTopAppBar(
        title = config.title,
        colors = TopAppBarDefaults.mediumTopAppBarColors(
            containerColor = containerColor
        ),
        modifier = modifier.shadow(elevation = elevation),
        navigationIcon = {
            config.navigationIcon()
        },
        actions = {
            config.actionIcon?.invoke()
        }
    )
}

ここでの核心概念

  1. ComposeネイティブのTopAppBar:CenterAlignedTopAppBarを使用

  2. 異なる画面では異なるトップバーの内容が必要になることを考慮し

    データクラスMainAppBarConfigを別途作成

    トップバーを使用する際に

    TopAppBarを繰り返し書く必要がなく

    MainAppBarConfigのインスタンスを作成するだけでいい

  3. よく使用される変数は外部に公開

    設定できるようにしています

    例:elevation

データクラスMainAppBarConfigの実装

よく調整される要素をカスタマイズできるようにします

タイトルの長さ、テキスト、スタイル、戻るアイコンなど

// in ~/commonMain/

data class MainAppBarConfig(
    val marqueeNum: Int = 0,
    val titleText: @Composable () -> String = { "" },
    val title: @Composable () -> Unit = {
        DefaultTitleText(titleText(), marqueeNum)
    },
    val navigationIcon: @Composable () -> Unit = {},
    val actionIcon: @Composable (() -> Unit)? = null,
)

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DefaultTitleText(titleText: String, marqueeNum: Int) {
    Text(
        modifier = Modifier.basicMarquee(marqueeNum),
        text = titleText,
        style = MaterialTheme.typography.titleMedium,
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
        color = ExtendedTheme.colors.onAppBar
    )
}
TopBarの実際の使用
* ここでは`createSetting`関数を作成します

前に作成したMainAppBarConfigを使用して

設定したい内容を入力します

// in ~/commonMain/

private fun createSettingConfig(
    navController: NavController,
) = MainAppBarConfig(
    titleText = { stringResource(Res.string.title_setting) },
    navigationIcon = {
        NavBackIcon(navController = navController)
    },
)

注:戻るボタンの遷移機能を実装したい場合

遷移イベントを関数に渡す必要があるかもしれません

ただし、NavControllerを使用するとより柔軟になります

NavControllerはすべてのルートを管理できます

遷移が必要なときに定義済みの文字列を指定するだけでいいです

この部分の詳細は後の章で説明します

  • 実際の使用
// in ~/commonMain/

@Composable
fun SettingScreen(navController: NavController) {
    val config = createSettingConfig(navController)

    Scaffold(
        topBar = {
            MainAppBar(config = config)
        },
        containerColor = MaterialTheme.colorScheme.surfaceVariant
    ) {...}
}

実際の例

  • 上記の概念を利用して

    簡単な設定画面を実装できます

// in ~/commonMain/

@Composable
fun SettingScreen(navController: NavController) {
    val config = createSettingConfig(navController)

    Scaffold(
        topBar = {
            MainAppBar(config = config)
        },
        containerColor = MaterialTheme.colorScheme.surfaceVariant
    ) { paddingValues ->
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            item {
                Text(
                    "Choose Transfer Option",
                    style = MaterialTheme.typography.bodySmall,
                    modifier = Modifier.padding(start = 30.dp, top = 16.dp, end = 30.dp)
                )
            }

            items(SettingOption.values()) { option ->
                SettingOptionCard(
                    option = option,
                    onClick = {
                        navController.navigate(option.route) {
                            navController.graph.startDestinationRoute?.let {
                                popUpTo(it) {
                                    saveState = true
                                }
                            }
                            launchSingleTop = true
                            restoreState = true
                        }
                    }
                )
            }
        }
    }
}