🙆‍♀️

Compose Multiplatformでの遷移方法を考える

2023/08/21に公開

はじめに

今回は、Compose Multiplatformを試している出てきた疑問点があり、色々と調べてわかったことについてまとめていきたいなと思います。
結論として、@cosmeアプリでは、FragmentのNavigationを使っているので、後述する外部のライブラリを使わない2の場合の実装パターンを採用しています。

疑問点

ComposeでViewの共通化ができるなら、もうAndroidとiOSほぼKotlinで書けそう。
ただ、遷移周りとかどうすればいいんだろう..?と思い、調べてみることにしました。

画面遷移といえば、AndroidだとNavigationを使うのかなと考えています。
試しに、sharedにてLibraryを入れてやってみたものの、自分はうまくいきませんでした。(見落としているだけかもしれないので、もし方法ありましたら教えていただけると嬉しいです。)

Decompose Router

どうやら公式ではまだなさそうだな、と思ったので、他の方法がないか調べていたところ個人で作られている方がいらっしゃいました。
Decompose Routerというライブラリで、これを使うとできるようですね。
Decompose Routerを実際に用いているサンプルも公開されてあるので、がっつり使うならこちらを参考にすると良いのでは、と思います。

Decompose Routerの内部のソースコードを追ってみたところ、Decomposeが元になっているようです。Decomposeは、Googleのエンジニアの方が作られているようで結構ドキュメントもまとまっています。

新規で作る個人開発のアプリであれば、上記ライブラリを使うと良さそうです。
しかし、メンテナンスが継続されるか不確かであることを踏まえると、既存の業務アプリに組み込むのはリスクがあるかもしれません。

既存との組み合わせ方を考える

とりあえずViewの共通化みたいなところはできそうと思ったので、遷移周りはネイティブに任せてしまってViewの表示だけやろうと思います。
サンプルを作ってみました。

結論からいうと、ComposeでView側を作ってネイティブで遷移まわりをやってしまうのはアリ、と思っています。

軽く作ってみたサンプルを解説していきます。

Androidの場合

テンプレートから作成してきたSharedに作ってあるmain.android.kt内部で、共通のComposableを呼び出す受け口となるComposable関数を用意します。

main.android.kt
import androidx.compose.runtime.Composable

@Composable fun FirstScreenView(navigationCallBack: () -> Unit) = FirstScreen(navigationCallBack)
@Composable fun SecondScreenView() = SecondScreen()

次に、Activity側で遷移処理を次のように書きます。これはshared側ではなく、KMPを利用するネイティブ側のコードになります。

MainActivity.kt
import FirstScreenView
import SecondScreenView
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            NavHost(navController = navController, startDestination = "first") {
                composable("first") {
                    FirstScreenView {
                        navController.navigate("second")
                    }
                }
                composable("second") {
                    SecondScreenView()
                }
            }
        }
    }
}

呼び出すComposable関数にコールバックを渡して、Activity側にて遷移処理を記載するだけです。
これを実行すると、次のような動作になります。

iOSの場合

テンプレートから作成してきたSharedに作ってあるmain.ios.kt内部で、共通のComposableを呼び出す受け口となる関数を用意します。

main.ios.kt
import androidx.compose.ui.window.ComposeUIViewController

fun FirstViewController(navigationCallBack: () -> Unit) = ComposeUIViewController { FirstScreen(navigationCallBack) }
fun SecondViewController() = ComposeUIViewController { SecondScreen() }

次に、iOS側で遷移処理を次のように書きます。これはshared側ではなく、KMPを利用するネイティブ側のコードになります。

ContentView.swift
import UIKit
import SwiftUI
import shared


struct ContentView: View {
    @State private var shouldShowSecondView: Bool = false
    
    var body: some View {
        NavigationView {
            VStack {
                FirstScreenView(navigationCallBack: {
                    shouldShowSecondView.toggle()
                })
                NavigationLink(destination: SecondScreenView(), isActive:$shouldShowSecondView) {
                    EmptyView()
                }
            }
        }
    }
}

呼び出す関数にコールバックを渡して、ContentView側で遷移の判断となるboolを変えるようにしているだけです。
これを実行すると次のようになります。

おわりに

Compose Multiplatformでの遷移どうしよう、という場合に取れる選択肢が二つあります。

  1. 個人のライブラリを使う場合はDecomposeをラップして作っているDecompose Routerを利用
  2. ネイティブ側で遷移処理を書きViewだけ共通化する方法

今回のサンプルだと2のパターンになっています。
サンプルでは、ComposeのNavigationを使っているのですが、FragmentのNavigationを使っている場合でも使えると思うのでそちらでも利用可能です。
個人プロジェクトであれば、1で良いような気がしていますが、すでにある既存プロダクトの場合なら2が良いのではないかと考えています。

株式会社アイスタイル

Discussion