はじめに
以前Flutterを使っていた経験から、現在はSESとしてiOSプログラマをしているuehatsuです。ちょっとCompose Multiplatformをいじる必要が出てきたので、連休を機会にさわり始めました。メモがてらまとめてみたいと思います。
環境構築
公式ホームページ通りにやったところ大きな問題はありませんでした。あえて言うなら以下の点で「あれ?」と思いました。
- Xcode 16.1をインストールした状態ではAndroid Studio上でシミュレータが正しく選べなかったので、Xcode 16.0にしたこと
- kdoctorがAndroid Studioのバージョンを正しく認識してくれなかったこと
- kdoctorが「Android Studio上で
Kotlin Multiplatform Mobile Plugin
をインストールしろ」と言うけど、そんなプラグインはなくKotlin Multiplatform Plugin
に改名されている、らしいこと
Multiplatform対応のプロジェクトを作成する
公式ページにはKotlin Multiplatform wizardを利用してプロジェクトを作成しろと書かれていました。以前私が触ったときはGitHubリポジトリをフォークする形だったので、便利になりましたね。当時もWizardがあったはずですが、野良のものだったのか、現在のJetBrains製のものかまでは失念してしまいました。
以下の設定でプロジェクトを作成しました。
- Project Name
- ComposeMultiplatformTest
- Project ID
- info.uehatsu.cmptest
- Android
- チェック
- iOS
- チェック
- Share UIを選択(Compose Multiplatformを使うので)
- Desktop, Web, Server
- 非チェック
最初Serverなどもチェックして作成したのですが、モジュールのインストールでエラーになってしまったので外しました。最終的には直るのでしょうが、初学者殺しですね(苦笑)。
プロジェクトを作成したらダウンロードしたZipファイルを展開し、git init, git add, git commitしておきます。
プロジェクトを開くとAGP Upgradeについて聞かれますが、行ってもGradle Version未検証のワーニングがでてしまうので、そのままにします。
今回行う事
順番が前後してしまいましたが、今回行う事を検討してみます。いくつかホームページを見たところ、株式会社Nextatさんのスタッフブログに行き着きました。ちょうど良い分量だったので、ここからいくつかピックアップして試してみたいと思います。
Compose Multiplatform 入門(環境構築編)
すでに環境構築は済んでいるのでスキップします。
Compose Multiplatform 入門(プロジェクト作成編)
こちらも既に済んでいるのでスキップします。
Compose Multiplatform入門(画面遷移編)
ここからやってみたいと思います。
この記事はKoinとVoyagerを使った画面遷移についてまとまっています。以下、記事と重複するところもありますが、私がやった手順や調べたことなどと共にまとめます。
Koinとは?
Kotlin用のDIプラットフォーム。基礎となるkoin-core
とそれ以外(例:koin-android
, koin-compose
, koin-ktor
)などに分かれている。
Voyagerとは?
マルチプラットフォーム用の画面遷移ライブラリ。単体でも簡単に画面遷移を記述する事ができるが、Koinと合わせることでさらに強力に画面遷移をわかりやすく記述することができる。
Activityを分けず、1つのActivityにViewControllerを集約し、その中で画面遷移を行う事で、簡単に画面の記述・追加などが行える。
セットアップ
記事には、libs.versions.toml
に記載する方法が取られていますが、実際に何をしているのかが不明瞭だったので、composeApp/build.gradle.kts
に記載した上で、Android Studioに修正してもらう方法を取ります。
kotlin {
sourceSets {
commonMain.dependencies {
// 追記
implementation("io.insert-koin:koin-core:4.0.0")
implementation("io.insert-koin:koin-compose:4.0.0")
implementation("cafe.adriel.voyager:voyager-koin:1.1.0-beta03")
implementation("cafe.adriel.voyager:voyager-navigator:1.1.0-beta03")
implementation("cafe.adriel.voyager:voyager-screenmodel:1.1.0-beta03")
}
}
}
追記するとAndroid Studioが波線をつけるので、マウスカーソルを重ねて、Replace with ...
を選択しlibs.versions.toml
にバージョン情報を移します。tomlファイルはversion.refを書き直しています。
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.voyager.koin)
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenmodel)
}
}
}
[versions]
koin = "4.0.0"
voyager = "1.1.0-beta03"
[libraries]
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
色々と準備が出来たら、build.gradle.kts
ファイルの右上に出ているSync Now
をクリックします。
ファイルの追加
記事の順番の通りにファイルを追加していくと補完が効かないので、ちょっと順番を変えていきます。また、ファイルの配置場所も若干変えています。
AnotherScreenViewModelクラスの追加
package info.uehatsu.cmptest.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
class AnotherScreenViewModel(val postId: String) : ScreenModel {
}
AnotherScreenクラスの追加
以下の変更をしています。
-
val ViewModel
-
val viewModel: AnotherScreenViewModel
に修正 - KoinのDI時の型を確定させるため
-
-
getScreenModel {}
-
koinScreenModel {}
に修正 - Deprecatedのため修正
-
package info.uehatsu.cmptest.presentation.screen.another
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import info.uehatsu.cmptest.viewmodel.AnotherScreenViewModel
import org.koin.core.parameter.parametersOf
class AnotherScreen(private val postId: String) : Screen {
@Composable
override fun Content() {
val viewModel: AnotherScreenViewModel = koinScreenModel { parametersOf(postId) }
val navigator = LocalNavigator.currentOrThrow
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = viewModel.postId)
Button(onClick = { navigator.pop() }) {
Text("戻る")
}
}
}
}
HomeScreenViewModelの追加
package info.uehatsu.cmptest.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
import info.uehatsu.cmptest.Greeting
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class HomeScreenViewModel(private val greeting: Greeting) : ScreenModel {
private val _showsContent = MutableStateFlow(false)
val showContent = _showsContent.asStateFlow()
private val _greet = MutableStateFlow(greeting.greet())
val greet = _greet.asStateFlow()
fun toggleContent() {
_showsContent.update { !it }
}
}
HomeScreenの追加 (WIP)
Screensを作成していないので、一旦ガワだけ作成します。
package info.uehatsu.cmptest.presentation.screen.home
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import info.uehatsu.cmptest.viewmodel.HomeScreenViewModel
class HomeScreen : Screen {
@Composable
override fun Content() {
val viewModel: HomeScreenViewModel = koinScreenModel()
val navigator = LocalNavigator.currentOrThrow
val showsContent by viewModel.showContent.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
}
}
}
Screensクラスの追加
以下変更しました。
-
register {}
-
register<Screens.Home> {}
に変更- KoinのDI型を明示的に指定
-
register<Screens.Another> {}
に変更- KoinのDI型を明示的に指定
-
package info.uehatsu.cmptest.presentation.screen
import cafe.adriel.voyager.core.registry.ScreenProvider
import cafe.adriel.voyager.core.registry.screenModule
import info.uehatsu.cmptest.presentation.screen.another.AnotherScreen
import info.uehatsu.cmptest.presentation.screen.home.HomeScreen
sealed class Screens : ScreenProvider {
data object Home : Screens()
data class Another(val postId: String) : Screens()
}
val appScreenModule = screenModule {
register<Screens.Home> {
HomeScreen()
}
register<Screens.Another> { provider ->
AnotherScreen(provider.postId)
}
}
HomeScreenクラスの修正
ガワだけ用意したHomeScreen
クラスを完成させます。
package info.uehatsu.cmptest.presentation.screen.home
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.registry.rememberScreen
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import composemultiplatformtest.composeapp.generated.resources.Res
import composemultiplatformtest.composeapp.generated.resources.compose_multiplatform
import info.uehatsu.cmptest.presentation.screen.Screens
import info.uehatsu.cmptest.viewmodel.HomeScreenViewModel
import org.jetbrains.compose.resources.painterResource
class HomeScreen : Screen {
@Composable
override fun Content() {
val viewModel: HomeScreenViewModel = koinScreenModel()
val navigator = LocalNavigator.currentOrThrow
val showsContent by viewModel.showContent.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val anotherScreen = rememberScreen(Screens.Another(postId = "dummy_post_id"))
Button(onClick = { navigator.push(anotherScreen) }) {
Text("画面遷移")
}
Button(onClick = { viewModel.toggleContent() }) {
Text("Click me!")
}
AnimatedVisibility(showsContent) {
val greeting by viewModel.greet.collectAsState()
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
}
}
ViewModelModulesの追加
package info.uehatsu.cmptest.module
import info.uehatsu.cmptest.Greeting
import info.uehatsu.cmptest.viewmodel.AnotherScreenViewModel
import info.uehatsu.cmptest.viewmodel.HomeScreenViewModel
import org.koin.dsl.module
val viewModelModule = module {
factory {
HomeScreenViewModel(get())
}
factory {
Greeting()
}
factory { params ->
AnotherScreenViewModel(postId = params.get())
}
}
AppModulesの追加
以下を変更しています。
- importを削除
- Listを削除
package info.uehatsu.cmptest.module
fun appModules() = listOf(viewModelModule)
Appクラスの更新
package info.uehatsu.cmptest
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.registry.ScreenRegistry
import cafe.adriel.voyager.core.registry.rememberScreen
import cafe.adriel.voyager.navigator.Navigator
import info.uehatsu.cmptest.module.appModules
import info.uehatsu.cmptest.presentation.screen.Screens
import info.uehatsu.cmptest.presentation.screen.appScreenModule
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.KoinApplication
@Composable
@Preview
fun App() {
KoinApplication(application = {
modules(appModules())
}) {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenRegistry {
appScreenModule()
}
val homeScreen = rememberScreen(Screens.Home)
Navigator(homeScreen)
}
}
}
}
実行
これでiOS, Androidとも実行が可能になりました。記事にはco.touchlab:stately-common
を入れるように書かれていますが、私の環境では入れなくても特にエラーになりませんでした。修正されたのかも知れませんね。
Compose Multiplatform 入門(ロギング編)
Kermitとは?
Kotlin用のロギングライブラリです。
セットアップ
今回はtomlファイルを直接いじって見たいと思います。修正したら、同期(Sync Now)しておきます。
[versions]
kermit = "2.0.4"
[libraries]
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kermit)
}
}
}
AnotherScreen.ktを修正する
先に作成したAnotherScreen.kt
を修正します。また以下修正を行いました。
-
LifecycleEffect()
-
LaunchedEffect(Unit) {}
に書き換え - Deplicatedのため
-
package info.uehatsu.cmptest.presentation.screen.another
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import co.touchlab.kermit.Logger
import info.uehatsu.cmptest.viewmodel.AnotherScreenViewModel
import org.koin.core.parameter.parametersOf
class AnotherScreen(private val postId: String) : Screen {
@Composable
override fun Content() {
val viewModel: AnotherScreenViewModel = koinScreenModel { parametersOf(postId) }
val navigator = LocalNavigator.currentOrThrow
LaunchedEffect(Unit) {
Logger.i { "Open Another Screen" }
}
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = viewModel.postId)
Button(onClick = { navigator.pop() }) {
Text("戻る")
}
}
}
}
実行
実行し、画面遷移ボタンをタップしてAnotherScreenが表示されると、Androidの場合、以下のようなログがLogCat
に表示されます。
iOSの場合はRunの実行Logに表示されます。
それ以降の記事
FirebaseLoginや、Ktorを使ったHTTP通信、リソースなどについて記事が続きます。今日はこの後予定があるので、明日改めて続編を書くかも知れません。
最後に
先にも書きましたが、参照した記事には続きがありました。明日また書くかも知れません。ひとまず「Compose Multiplatformの最初の一歩」はクリアした感じ。ここからJetpack Composeや、実際のKMP, CMPのコアな部分も触ってみたいと思っています。