Compose Multiplatform: 画面遷移ライブラリを評価する ⑤ まとめ編

以下の記事ではCompose Multiplatformの画面遷移ライブラリについて調査してきました。

調査をしてきた中で、現時点で画面遷移ライブラリを使うのであれば、どれがよいのか実装・調査結果をふまえて、個人的な意見を以下にまとめます。

目次

実装を比較してみる

Voyager・PreCompose・Decompose Routerの実装を並べて、どれが一番画面定義がわかりやすいか比較してみましたが、個人的にはPreCompose・Decompose Routerがわかりやすそうかなと思いました。

Voyagerは少ない画面であれば、問題なく処理を追うことができそうですが、画面数が多くなると処理を追うことが難しくなりそうなので、実装のわかりやすさを優先するならPreComposeやDecompose Routerを利用するのが良さそうと感じました。

項目 行数(空白行を除く) 画面定義のわかりやすさ 説明
Voyager 38 課題あり – 画面ごとのクラスを宣言するため全体像がわかりにくい
– ダイアログは画面に埋め込む必要がるので見にくい
PreCompose 46 良い – 画面やダイアログが一覧で宣言されるのでわかりやすい
– 画面ごとのRouteを定義するところが若干の見にくさがある(工夫しだいでどうにでもなりそうですが)
Decompose Router 37 最高 – 画面を表現するsealed classを宣言しているので、全体像が掴みやすくわかりやすい
– ダイアログは画面に埋め込む必要があるので見にくい

Voyager

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

class GalleryScreen : Screen {
    @Composable
    override fun Content() {
        val navigator = LocalNavigator.current
        GalleryContent(
            pictures = Pictures.value,
            onClick = { navigator?.push(PreviewScreen(it)) },
            modifier = Modifier.fillMaxSize()
        )
    }
}

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 },
            modifier = Modifier.fillMaxSize(),
        )

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

PreCompose

Kotlin
@Composable
fun PrecomposeDemo() {
    PreComposeApp {
        val navigator = rememberNavigator()
        NavHost(
            navigator = navigator,
            navTransition = NavTransition(),
            initialRoute = "/gallery"
        ) {
            scene(
                route = "/gallery",
                navTransition = NavTransition(),
            ) {
                GalleryScreen(
                    pictures = Pictures.value,
                    onNavigatePreview = { navigator.navigate("/preview/${it.name}") },
                    modifier = Modifier.fillMaxSize()
                )
            }

            scene(
                route = "/preview/{name}",
                navTransition = NavTransition(),
            ) { backStackEntry ->
                val name = backStackEntry.path<String>("name")
                val picture = Pictures.value.firstOrNull { it.name == name } ?: return@scene

                PreviewScreen(
                    picture = picture,
                    onNavigateDetails = { navigator.navigate("/details/${it.name}") },
                    onBack = { navigator.popBackStack() },
                    modifier = Modifier.fillMaxSize()
                )
            }

            dialog(
                route = "/details/{name}"
            ) { backStackEntry ->
                val name = backStackEntry.path<String>("name")
                val picture = Pictures.value.firstOrNull { it.name == name } ?: return@dialog
                DetailsDialog(
                    picture = picture,
                    onBack = { navigator.popBackStack() },
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

Decompose Router

Kotlin
@Parcelize
sealed class Screens : Parcelable {
    data object Gallery : Screens()
    data class Preview(val name: String) : Screens()
}

@Composable
fun DecomposeApp() {
    val router: Router<Screens> = rememberRouter(Screens::class) { listOf(Screens.Gallery) }
    RoutedContent(router = router) { screen ->
        when (screen) {
            Screens.Gallery -> {
                GalleryScreen(
                    pictures = Pictures.value,
                    onNavigatePreview = { router.push(Screens.Preview(it.name)) },
                    modifier = Modifier.fillMaxSize()
                )
            }

            is Screens.Preview -> {
                val picture = Pictures.value.firstOrNull { it.name == screen.name } ?: return@RoutedContent
                var isShowDetails by remember { mutableStateOf(false) }

                PreviewScreen(
                    picture = picture,
                    onNavigateDetails = { isShowDetails = true },
                    onBack = { router.pop() },
                    modifier = Modifier.fillMaxSize()
                )

                if (isShowDetails) {
                    DetailsDialog(
                        picture = picture,
                        onBack = { isShowDetails = false },
                        modifier = Modifier.wrapContentSize()
                    )
                }
            }
        }
    }
}

評価を比較してみる

Voyager・PreCompose・Decompose Routerの評価を並べて、どれが一番扱いやすそうか比較してみました。どのライブラリも一長一短あるのですが個人的には「簡単なアプリを作るのでVoyager」「多少複雑なアプリを作るならPreCompose」「多少複雑なアプリを作りたい、細かなプラットフォームのカスタマイズをしたいならDecompose Router」を使うべきかなと感じました。

Decompose Routerは画面遷移の実装が一番しやすいと感じるものの、Parcelizeなどの仕組みをEssentyというライブラリに頼っていることもあり、そのあたりの今後の実装変更リスクなどありそうで気になります。そのあたりを考慮してCompose Multiplatformをこれから始める方であればVoyagerかPreComposeを利用するのが良いのかなと感じました。

項目 Voyager 評価 PreCompose 評価 Decompose Router 評価
セットアップのしやすさ 最高 最高 課題あり
途中で移行しやすいか 課題あり 最高 最高
画面定義の実装しやすさ 最高 最高 最高
画面遷移の実装しやすさ 課題あり 課題あり 最高
画面遷移のパラメータ渡しやすさ 最高 課題あり 良い

Voyager

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

PreCompose

項目 評価(最高・良い・課題あり) 理由など
セットアップのしやすさ 最高 ・依存関係を追加して、ルートにPreComposeApp・NavHostを配置するだけ、なので特に難しくもなく良さそう
途中で移行しやすいか 最高 ・各画面やダイアログの実装をsceneやdialogで登録するだけなので、もし画面遷移ライブラリを変えようとした場合にでも、問題なく移行が可能
画面定義の実装しやすさ 最高 ・Composable関数に画面やダイアログを実装するだけなので特に難しいことはない
画面遷移の実装しやすさ 課題あり ・sceneやdialogで画面を登録していく形式で、sceneやdialogにはrouteを指定する必要がある。routeの構造やパラメータなどの定義をするのがやや難しがある
・またNavigatorのnavigateを呼び出す時にrouteを生成して渡す必要もあるので、その辺りの実装方針を決めたりする必要があるので手間がかかりそう
画面遷移のパラメータ渡しやすさ 課題あり ・routeにパラメータを埋め込む形式であるため基本的にはプリミティブ型しか渡せない構造になっている。
・もしdata classなどのオブジェクトを渡したいときには文字列型に変換するなどの処理が必要になるため決して渡しやすい構造ではない

Decompose Router

項目 評価(最高・良い・課題あり) 理由など
セットアップのしやすさ 課題あり ・ParcelizeやRouterContextのセットアップがややわかりづらく保守がやりづらいと感じる
・Essentyが提供するParcelizeの仕組みがKotlin v2移行に終了する可能性があり移行が必要になる可能性が高そう
途中で移行しやすいか 最高 ・ParcelizeなScreensクラスを定義する形式なので、もし画面遷移ライブラリを変えようとした場合にでも、問題なく移行が可能
画面定義の実装しやすさ 最高 ・ParcelizeなScreensクラスを定義して、サブクラスごとに画面を定義する方式なのでわかりやすい。
・各画面の定義についてはComposable関数に画面やダイアログを実装するだけなので特に難しいことはない
画面遷移の実装しやすさ 最高 ・ParcelizeなScreensクラスを生成してRouterして渡すことで、画面遷移を実行する形式なので、わかりやすい。
画面遷移のパラメータ渡しやすさ 良い ・ParcelizeなScreensクラスを生成する際にパラメータを渡すのでわかりやすいが、渡すパラメータはプリミティブ型かParcelizeなクラスでなければいけないので少々ややこしい

個人的におすすめのライブラリ

上記の比較をふまえてVoyager・PreCompose・Decompose Routerをどの順番でおすすめするか決めるとすると以下の順番になるかなと思います。

  1. PreCompose
  2. Voyager
  3. Decompose Router

PreComposeがなぜ1番か?

PreComposeはJetpack ComposeのNavigation Composeによく似ているライブラリで以下のメリットがあるのと、全体的にバランスが取れた画面ライブラリなので1番目におすすめしたいなと感じました。

  • PreComposeはNavigation Composeに似ているため、AndroidエンジニアでNavigation Composeを使ったことがある人であれば、すぐに利用開始できる
  • またNavigation Composeに似ているので、Navigation Composeで培ったノウハウなどが、PreComposeで活かせるのでRouteの設計方法やパラメータのシリアライズ方法など、このあたりの資料が調べれば出てくるのが心強い。

Voyagerがなぜ2番か?

Voyagerは少し画面遷移の定義方法に癖があり、複雑な画面遷移を持つアプリを作れるのか、不安なところがあるかなと思っています。簡単なアプリだとスピーディーに作れる仕組みが用意されているのですが、複雑な画面遷移の対応が難しそうとうことで、2番目におすすめしたいと感じました。

Decompose Routerがなぜ3番か?

Decompose Routerは画面遷移の実装しやすさでは一番なのですが、セットアップ手順が複雑であることや、依存先のEssentyが提供するParcelizeの仕組みが今後提供されつづけるのか、心配なところも多いので3番目のおすすめしたいと感じました。

おわりに

今回の調査は私の個人的な評価で、評価している項目に完全な公平性があるわけではないのですが、個人的にはPreComposeを利用しておけば、Compose Multiplatformの開発においては間違い無いんじゃないかなと感じました。

Compose Multiplatformの画面遷移ライブラリはいろんなものがあり、どのように画面遷移を実現するかは手探りな状態かなと思うので、PreComposeに関しても今後利用を続けてみて、本当に最適解なのか検証を進められたらと思います。

参考文献