開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. Kotlin >
  4. Compose Multiplatform入門(画面遷移編)

Compose Multiplatform入門(画面遷移編)

logo.png

こんにちは、ホンジョウです。
今回は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を更新します。
sync_gradle.png
メニューからも実行することができます。
sync_gradle_menu.png

実装設計についての概要

  • 各画面はそれぞれ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両方で動作が確認できるかと思います。
screen_transition_play.png

最後に

次回はロギングについての記事になります。ご覧いただきありがとうございました。
  • posted by りっちゃん
  • Kotlin
TOPに戻る