🧭

Compose for Web でアドレスバーのURLとCompose Navigationのrouteを同期させてみる試み

2024/11/07に公開

最近の日課が KotlinJSとお戯れすることになっているてべすてんです。

https://x.com/tbs__ten/status/1853826971190644787

これ is なに?

Compose for Web を使うことで Webの知識なしに Webアプリを作ることができます。

しかし そのままでは ブラウザのアドレスバーは考慮されず、画面遷移してもURLが変わることはありません。これでは リロードすると最初の画面に戻ってしまったり、特定の画面のURLをコピーするといったことができません。

そこでCompose for Web で SPA のような動きをするアプリができないか?といったチャレンジの記録になります。

今回はブラウザに標準で用意されている History API を使って ブラウザのURLを変更することで、ブラウザのアドレスバーのURLとアプリの画面を同期させてみました。

サンプル


ポイント:アドレスバーが画面遷移に応じて同期されている

(JSでビルドしてあるため、パフォーマンスは若干劣りますが、Safariでも動くはずです!)

https://cmp-history-api-sample-js.vercel.app/

https://github.com/TBSten/cmp-history-api-sample

使用技術

  • Kotlin: 2.0.21
  • Compose Multiplatform
  • Navigation Compose: 2.8.0-alpha10
  • History API
  • Vercel

仕組み

Navigation Compose の画面遷移 と アドレスバー との同期には 以下の4つを考慮する必要があります。

  1. Navigation Composeの画面遷移に応じてアドレスバーのURLを書き換える。( Navigation Comopose -> アドレスバー
  2. アドレスバーの変化に合わせてNavigation Composeを画面遷移させる。( アドレスバー -> Navigation Compose
  3. どのページに来ても index.html に遷移させる
  4. 最初のURLに合わせて startDestination を変更する。

サンプルのリポジトリでは NavController.bindBrowserUrl() と言う拡張関数を中心に

https://github.com/TBSten/cmp-history-api-sample/blob/34e6e02212302cddb5c59fd39ddcd72ee8806496/composeApp/src/jsMain/kotlin/me/tbsten/sample/cmpHistoryApi/navigation/Navigation.js.kt#L15-L38

1. Navigation Comopose -> アドレスバー の同期

bindBrowserUrl 関数内では NavController の BackStackEntry を監視し、 変更があった際に window.history.pushState() と言う関数を呼び出しています。

window.history.pushState() はサーバにリクエストを送ることなく アドレスバーのURLのみを変更できる History API の関数です。

このようにすることで Navigation Compose の画面遷移に合わせてアドレスバーのURLを書き換える Navigation Comopose -> アドレスバー の同期ができました。

https://github.com/TBSten/cmp-history-api-sample/blob/34e6e02212302cddb5c59fd39ddcd72ee8806496/composeApp/src/jsMain/kotlin/me/tbsten/sample/cmpHistoryApi/navigation/Navigation.js.kt#L20-L29

2. アドレスバー -> Navigation Compose の同期

続いてアドレスバーのURLが変更された時に Navigation Composeに反映する方法です。

これには History API の popstate イベントを使用します。

addEventListener("popstate", { TODO() })

のように書くことで アドレスバーのURLの変更を検知できます。

あとはこれに応じてNavControllerで遷移させる処理を書けば アドレスバー -> Navigation Compose の同期も完成です。

https://github.com/TBSten/cmp-history-api-sample/blob/34e6e02212302cddb5c59fd39ddcd72ee8806496/composeApp/src/jsMain/kotlin/me/tbsten/sample/cmpHistoryApi/navigation/Navigation.js.kt#L30-L37

3. どのページに来ても index.html に遷移させる

今回のようなSPAでは 1つのHTMLファイルで全てのページを処理します。なので例え存在しないパスに来たとしても 必ず index.html を返すようにする必要があります。

これにはHTTPサーバの設定が必要です。

今回のサンプルは元々 ホスティングサービスである Vercel にデプロイすることを念頭に置いていたため、Vercelの rewrite 機能を使ってみることにします。

プロジェクトルートに vercel.json を配置することで簡単に設定することができます。 どのページに来てもとは言いましたが JSファイルまでindex.htmlに飛ばされてしまうとComposeのコードが実行されないため これはrewriteの対象外にする必要がある 点には注意すると以下のようになるでしょう。

https://github.com/TBSten/cmp-history-api-sample/blob/main/vercel.json

  • "buildCommand" はデプロイする際に実行されるコマンドです。通常は npm build などを設定するかと思いますが、 kotlin jsでは ./gradlew :composeApp:jsBrowserDistribution と言うコマンドで本番用のHTMLやJSを出力するためこのように設定しています。
  • "devCommand"vercel dev でテストをする際に実行するコマンドです。Vercelにデプロイした際の挙動をローカル環境でテストすることができます。kotlin jsでは ./gradlew :composeApp:jsRun と言うコマンドで開発を行うためこのように設定しています。
    • vercel コマンドの実行にはVercel CLIの事前インストールが必要です。npmがインストールされている場合 npm i -g vercel で簡単にインストールできます。
    • とは言うものの、rewriteの設定は vercel dev では反映されないのか 確認できませんでした。rewriteの設定を検証するには vercel deploy コマンドで実際にデプロイしなければいけなさそうです。

より詳しい情報は Vercelのドキュメント を参照してください。

4. 最初のURLに合わせて startDestination を変更する

上記の設定をすることで どんなURLでアクセスされても index.html をレスポンスすることができるようになります。

あとは URLに合わせて NavHostの startDestination を書き換えましょう。

https://github.com/TBSten/cmp-history-api-sample/blob/34e6e02212302cddb5c59fd39ddcd72ee8806496/composeApp/src/jsMain/kotlin/main.kt#L17-L21

完成!

これでCompose for WebでSPAのようなアプリができるようになるはずですー! ヤッタネ!😎

良いCMPライフを!

Discussion