Compose Multiplatform: 画面遷移ライブラリを評価する ② Voyager編

本記事の概要はこちらで確認ください。今回はVoyagerを利用した画面遷移について調べていきます。

https://github.com/adrielcafe/voyager

目次

環境

この記事では以下の環境で実装を進めています。

名称 バージョン 備考
Fleet(IDE) Fleet version: build 1.26.104 OS: Mac OS X (14.1.1, aarch64)
Kotlin 1.9.20
Compose Multiplatform 1.5.4

※もし不明点がある場合はこちらのGitHubのソースコードにて詳細を確認ください。

https://github.com/kaleidot725/Compose-Multiplatform-Playground/tree/main/projects/Navigation

セットアップ

Voyagerを利用するには依存関係のセットアップが必要になるのでセットアップしていきます。

依存関係

TOML
[versions]
voyager = "1.0.0-rc10"

[libraries]
compose-voyager = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }

Kotlin
kotlin {
    // … 省略
    sourceSets {
        // … 省略
        commonMain.dependencies {
            // … 省略
            implementation(libs.compose.voyager)
        }
    }
}

画面定義

画面

Voyagerで画面を定義するには、Voyagerが提供するScreenを継承したクラスを画面ごとに定義する必要があります。

Kotlin
public actual interface Screen : Serializable {
    public actual val key: ScreenKey
        get() = commonKeyGeneration()

    @Composable
    public actual fun Content()
}

今回必要な画面は「ギャラリー画面」「プレビュー画面」の2つになるのでScreenを継承して下記のように定義してやります。

ギャラリー画面

Kotlin
class GalleryScreen : Screen {
    @Composable
    override fun Content() {
        GalleryContent(
            pictures = Pictures.value,
            onClick = { /** TODO プレビュー画面への遷移の実装が必要 */ },
            modifier = Modifier.fillMaxSize()
        )
    }
}

プレビュー画面

Kotlin
class PreviewScreen(
    private val picture: Picture
) : Screen {
    @Composable
    override fun Content() {
        PreviewContent(
            picture = picture,
            onBack = { /** TODO ギャラリー画面へ戻る遷移の実装が必要 */ },
            onNavigateDetails = { /** TODO 詳細情報ダイアログへの遷移の実装が必要 */ },
            modifier = Modifier.fillMaxSize(),
        )
    }
}

ダイアログ

調べたところVoyagerでダイアログの定義にはサポートしていませんでした。(Screenを継承したクラスにダイアログを表示する方式も試してみましたが背景が透過されずうまくいかず)

https://github.com/adrielcafe/voyager/issues/73

最終的にVoyagerでダイアログを表示する一つの方式として、ダイアログを表示する画面のScreenに埋め込むという方法を見つけました。なので下記のように「プレビュー画面」に「詳細情報ダイアログ」を埋め込む形で実装をすることにしました。

詳細情報ダイアログ

Kotlin
class PreviewScreen(
    private val picture: Picture
) : Screen {
    @Composable
    override fun Content() {
        var isShowDeitals by remember { mutableStateOf(false) }
        PreviewContent(
            picture = picture,
            onBack = { /** TODO ギャラリー画面へ戻る遷移の実装が必要 */ },
            onNavigateDetails = { isShowDeitals = true },
            modifier = Modifier.fillMaxSize(),
        )

        if (isShowDeitals) {
            Dialog(onDismissRequest = { isShowDeitals = false }) {
                DetailsContent(
                    picture = picture,
                    onBack = { isShowDeitals = false },
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

画面遷移

ルート設定

Voyagerで画面遷移を実行するには、Voyagerが提供するComposable関数のNavigatorを配置する必要があります。

Kotlin
public fun Navigator(
    screen: Screen,
    disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(),
    onBackPressed: OnBackPressed = { true },
    key: String = compositionUniqueId(),
    content: NavigatorContent = { CurrentScreen() }
) { ... }

そのため下記のようにアプリケーションのルートにNavigatorを配置します。

またNavigatorには最初に表示するScreenを渡す必要があるので「プレビュー画面」のScreenを作成して渡してあげまう。

Kotlin
@Composable
fun VoyagerApp() {
    Navigator(GalleryScreen())
}

特定の画面に遷移する 

Voyagerで特定の画面へ遷移するには、Voyagerが提供するクラスのNavigatorを利用します。

Kotlin
public class Navigator @InternalVoyagerApi constructor(
    screens: List<Screen>,
    @InternalVoyagerApi public val key: String,
    private val stateHolder: SaveableStateHolder,
    public val disposeBehavior: NavigatorDisposeBehavior,
    public val parent: Navigator? = null
) : Stack<Screen> by screens.toMutableStateStack(minSize = 1) {}

このNavigatorにはpushというメソッドがあるのでこれを使って特定の画面への遷移します。

※ソースコードを見ると下記のようなコードは定義されていませんが、便宜上わかりやすく下記のように表記しています。

Kotlin
fun push(screen : Screen)

今回は「ギャラリー画面」から「プレビュー画面」への遷移が必要になります。

なので「ギャラリー画面」でNavigatorをLocalContextで取得し、「プレビュー画面」への遷移をpushで実行するようにします。

「ギャラリー画面」から「プレビュー画面」への遷移

Kotlin
class GalleryScreen : Screen {
    @Composable
    override fun Content() {
        val navigator = LocalNavigator.current // LocalNavigatorでNavigatorを取得する
        GalleryContent(
            pictures = Pictures.value,
			// Navigatorのpushで画面遷移する。
            // pushにはScreenを渡す必要があるので、必要なパラメータを渡して渡してあげる。
            onClick = { navigator?.push(PreviewScreen(it)) },             
			modifier = Modifier.fillMaxSize()
        )
    }
}

1つ前の画面に戻る

Voyagerで1つ前の画面に戻るには遷移と同じで、Voyagerが提供するクラスのNavigatorを利用します。

Kotlin
public fun Navigator(
    screen: Screen,
    disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(),
    onBackPressed: OnBackPressed = { true },
    key: String = compositionUniqueId(),
    content: NavigatorContent = { CurrentScreen() }
) { ... }

このNavigatorにはpopというメソッドがあり、これを使って1つ前の画面に戻ります。

※ソースコードを見ると下記のようなコードは定義されていないのですが、便宜上わかりやすく下記のように表記しています。

Kotlin
fun pop()

今回は「プレビュー画面」から1つ前の「ギャラリー画面」に戻る必要になります。

なので「プレビュー画面」でNavigatorをLocalContextで取得し、「ギャラリー画面」に戻る遷移をpopで実行するようにします。

「プレビュー画面」から1つ前の「ギャラリー画面」に戻る

Kotlin

class PreviewScreen(
    private val picture: Picture
) : Screen {
    @Composable
    override fun Content() {
        val navigator = LocalNavigator.current // LocalNavigatorでNavigatorを取得する
        var isShowDeitals by remember { mutableStateOf(false) }

        PreviewContent(
            picture = picture,
			// Navigatorのpopで1つ前の画面に戻る。
            onBack = { navigator?.pop() },
            onNavigateDetails = { isShowDeitals = true },
            modifier = Modifier.fillMaxSize(),
        )

        if (isShowDeitals) {
            Dialog(onDismissRequest = { isShowDeitals = false }) {
                DetailsContent(
                    picture = picture,
                    onBack = { isShowDeitals = false },
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

特定のダイアログに遷移する

前述の通りVoyagerではダイアログの画面定義・遷移についてはサポートしていないようでした。今回は「プレビュー画面」から「情報詳細ダイアログ」への遷移が必要でしたが、以下のように表示フラグ(isShowDetails)を制御することでダイアログを表示しています。

「プレビュー画面」で「情報詳細ダイアログ」を制御する

Kotlin
class PreviewScreen(
    private val picture: Picture
) : Screen {
    @Composable
    override fun Content() {
        val navigator = LocalNavigator.current
        var isShowDeitals by remember { mutableStateOf(false) } // 表示フラグを定義する

        PreviewContent(
            picture = picture,
            onBack = { navigator?.pop() },
            onNavigateDetails = { isShowDeitals = true }, // 情報詳細ボタンがクリックされたら、表示フラグをtrueにして表示する
            modifier = Modifier.fillMaxSize(),
        )

        // 表示フラグで表示・非表示を切り替える
        if (isShowDeitals) {
        // ダイアログ外の領域をタップ、もしくは閉じるを押した時に表示フラグをfalseにして非表示する
            Dialog(onDismissRequest = { isShowDeitals = false }) {
                DetailsContent(
                    picture = picture,
                    onBack = { isShowDeitals = false },
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

動作確認

ここまで実装できたら動作確認をします。Voyagerを利用するとこのような感じで「ギャラリー画面」「プレビュー画面」「情報詳細ダイアログ」で画面遷移できます。

Android
iOS

個人的な評価

Voyagerは画面遷移ライブラリの中でも使いやすく入門者にはベストなライブラリだと感じました。ですが画面数が多いアプリや画面遷移が複雑なアプリでは実装が難しくなりそうな雰囲気があるので、比較的簡単なアプリを作るときにはVoyagerを利用して作るのが良いかなと感じました。

メリット

  • Voyagerではセットアップ・画面定義は簡単にできる
  • Voyagerでは単純な画面遷移であれば簡単にできる

デメリット

  • VoyagerではScreenを継承したクラスに画面を実装する、また各Screenに画面遷移処理を埋め込む、そのため他の画面遷移ライブラリへの移行は難しい
  • また各Screenに画面遷移処理を埋め込むので、画面遷移処理が散らばりやすく、画面数が多いアプリ・画面遷移が複雑なアプリでは実装が難しくなりそう

評価詳細

項目 評価(最高・良い・課題あり) 理由など
セットアップのしやすさ 最高 ・依存関係を追加して、ルートにNavigatorを配置するだけ、なので特に難しくもなく良さそう
途中で移行しやすいか 課題あり ・Voyagerが提供するScreenクラスを画面ごとに用意する必要があり、この画面定義を別の画面遷移ライブラリに移行するのは、かなり面倒そう。
・Voyagerで画面遷移するには各ScreenクラスでNavigatorを取得して、画面遷移の処理を実行する必要がある。各Screenクラスに画面遷移処理が散らばることになるので、画面遷移ライブラリの移行はかなり面倒になりそう。
画面定義の実装しやすさ 最高 ・Screenクラスに画面を定義する方式はわかりやすく良さそう
画面遷移の実装しやすさ 課題あり ・Voyagerで画面遷移するには各ScreenクラスでNavigatorを取得して、画面遷移の処理を実行する形式だが、基本的な画面遷移は実現できそうなので良い。
・だがScreenクラスに画面遷移処理を記述する方式であるため、特定の条件を満たしたら強制的に画面遷移するなどの複雑な制御を実現するのは難しそう。
・また画面遷移処理がScreenクラスに散らばることもあるので、画面遷移の全容を把握しずらいといった課題もありそう。
画面遷移のパラメータ渡しやすさ 最高 ・NavigatorのpushでScreenを生成する形式なので、Screenを生成する際に直接パラメータを渡せるのは最高に良い。

おわりに

今回はVoyagerについて評価してみました、次はPreComposeについて評価してみたいと思います。

参考文献