Compose Multiplatform入門(画面遷移編)
こんにちは、ホンジョウです。
今回はCompose Multiplatform 入門記事の第3回目ということで、画面遷移処理の実装に進んでいきたいと思います。
KoinとVoyagerを使用した画面遷移
Koinとは
Kotlinのマルチプラットフォーム向けのDI用ライブラリです。今回はKoinを使用して各画面の依存性注入をしていきます。https://github.com/InsertKoinIO/koin
Voyagerとは
マルチプラットフォーム向けの画面遷移ライブラリです。画面の移動処理を簡単に書くことができるようになります。画面構成をSingle-Activityにするためのライブラリで、AndroidアプリのActivityやiOSアプリのViewControllerを一つにしてその中で画面遷移を行うことで、簡単に画面の追加などができるようになります。
https://voyager.adriel.cafe/
セットアップ
Koin、Voyagerをプロジェクトで使用できるように、libs.versions.tomlとcomposeApp/build.gradle.ktsを更新します。libs.versions.toml
[versions]
voyager = "1.0.0"
koin = "3.5.4"
[libraries]
koin-bom = { group = "koin", module = "io.insert-koin:koin-bom", version.ref = "koin"}
koin-core = { group = "koin", module = "io.insert-koin:koin-core" }
koin-compose = { group = "koin", module = "io.insert-koin:koin-compose" }
voyager-screen-model = { group = "voyager", module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
voyager-navigator = { group = "voyager", module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-koin = { group = "voyager", module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
composeApp/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.voyager.screen.model)
implementation(libs.voyager.navigator)
implementation(libs.voyager.koin)
}
}
}
これらを変更すると、以下のメッセージが表示されるので、「Sync Now」をクリックしてGradleを更新します。メニューからも実行することができます。
実装設計についての概要
- 各画面はそれぞれVoyagerが提供するScreenクラスを継承したViewクラスとして管理する
- データの処理などを行うViewModelクラスを、Voyagerが提供するScreenModelクラスを継承して、Viewクラスと1対1の関係で追加する
- ViewModelModulesというクラスを追加し、DI処理を書く
- ViewModelはVoyagerによってライフサイクルが管理されているため、DIする時はsingleでは登録せず、factoryにする
HomeScreenの追加
今回HomeScreenは、Kotlin Multiplatform Wizardでダウンロードしたプロジェクトに入っていたUIに、他の画面へ遷移するボタンを追加したものにします。場所:commonMain/kotlin/ui/screens/home/HomeScreen.kt
package ui.screens.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.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import multiplatform_practice.composeapp.generated.resources.Res
import multiplatform_practice.composeapp.generated.resources.compose_multiplatform
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
import ui.screens.Screens
class HomeScreen : Screen {
@OptIn(ExperimentalResourceApi::class)
@Composable
override fun Content() {
// getScreenModel()は、 VoyagerとKoinを連携して、ScreenModelを取得してくれる
val viewModel: HomeScreenViewModel = getScreenModel()
// LocalNavigatorが画面の管理と遷移処理を行う
val navigator = LocalNavigator.currentOrThrow
val showsContent by viewModel.showsContent.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Voyagerが用意しているrememberScreen()を使って、Composable関数の中で画面を取得できる
val anotherScreen = rememberScreen(Screens.Another(postId = "dummy_post_id"))
// navigator.push() で画面遷移先を指定する
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")
}
}
}
}
}
HomeScreenViewModelの追加
HomeScreenViewModelに、HomeScreenで行う処理の部分を書いていきます。場所:commonMain/kotlin/ui/screens/home/HomeScreenViewModel.kt
package ui.screens.home
import Greeting
import cafe.adriel.voyager.core.model.ScreenModel
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 showsContent = _showsContent.asStateFlow()
private val _greet = MutableStateFlow(greeting.greet())
val greet = _greet.asStateFlow()
fun toggleContent() {
_showsContent.update { !it }
}
}
AnotherScreenクラスの追加
AnotherScreenには戻るボタンのみ用意しておきます。HomeScreenから遷移する際に、画面にパラメータを渡す仕組みも確認しておきたいので、Koinが提供しているparametersOf()を使って値を受け取っています。
場所:commonMain/kotlin/ui/screens/another/AnotherScreen.kt
package ui.screens.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.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import org.koin.core.parameter.parametersOf
class AnotherScreen(private val postId: String) : Screen {
@Composable
override fun Content() {
val viewModel = getScreenModel{ parametersOf(postId) }
val navigator = LocalNavigator.currentOrThrow
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = viewModel.postId)
// navigator.pop()で前の画面に戻る
Button(onClick = { navigator.pop() }) {
Text("戻る")
}
}
}
}
AnotherScreenViewModelクラスの追加
今回は一旦クラスの用意だけしておき、処理は書かないでおきます。場所:commonMain/kotlin/ui/screens/another/AnotherScreenViewModel.kt
package ui.screens.another
import Greeting
import cafe.adriel.voyager.core.model.ScreenModel
class AnotherScreenViewModel(val postId: String) : ScreenModel {
}
Screensクラスの追加
各種画面を管理するScreensクラスを用意します。ScreensクラスはVoyagerのScreenProviderインターフェースの実装で、アプリケーションで使われるすべての画面をこのクラスに列挙します。
screenModuleはScreenRepositoryのエイリアスで、各種画面の登録をしていきます。引数に何を渡すかもここに記述します。
場所:commonMain/kotlin/ui/screens/Screens.kt
package ui.screens
import cafe.adriel.voyager.core.registry.ScreenProvider
import cafe.adriel.voyager.core.registry.screenModule
import ui.screens.another.AnotherScreen
import ui.screens.home.HomeScreen
sealed class Screens : ScreenProvider {
data object Home : Screens()
data class Another(val postId: String) : Screens()
}
val appScreenModule = screenModule {
register {
HomeScreen()
}
register { provider ->
AnotherScreen(provider.postId)
}
}
ViewModelModulesの追加
ViewModelModulesでKoinのDI処理を書いていきます。場所:commonMain/kotlin/modules/ViewModelModules.kt
package modules
import Greeting
import org.koin.dsl.module
import ui.screens.another.AnotherScreenViewModel
import ui.screens.home.HomeScreenViewModel
val viewModelModule = module {
factory {
HomeScreenViewModel(get())
}
factory {
Greeting()
}
factory {
params -> AnotherScreenViewModel(postId = params.get())
}
}
AppModulesの追加
各種Moduleを管理するAppModulesを追加します。今回はViewModelModulesしかありませんがいずれ増えていきます。
場所:commonMain/kotlin/modules/AppModules.kt
package modules
import org.koin.core.module.Module
fun appModules(): List = listOf(viewModelModule)
Appクラスの更新
AppクラスをVoyager、Koinを使用した画面表示処理に書き直します。
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 modules.appModules
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.KoinApplication
import ui.screens.Screens
import ui.screens.appScreenModule
@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)
}
}
}
}
この時点で、Androidでの画面遷移は可能になっています。このままではiOSビルドでエラーが出てしまうので、iOSビルド用に修正を加えます。
iOSビルドでのエラー修正
以下のエラーが出るので、修正します。
Task :composeApp:compileKotlinIosX64 FAILED
error: Could not find "co.touchlab:stately-common" in [プロジェクトディレクトリ]
error: Compilation finished with errors
ライブラリを追加すれば動きます。
libs.versions.toml
[versions]
stately = "2.0.7"
[libraries]
stately-common = { module = "co.touchlab:stately-common", version.ref = "stately" }
build.gradle.kts
kotlin {
sourceSets {
iosMain.dependencies {
implementation(libs.stately.common)
}
}
}
これでAndoroid、iOS両方で動作が確認できるかと思います。