この記事は、「NEWT Product Advent Calendar 2024」Day9 および「Android Advent Calendar 2024」Day12の記事となります。
こんにちは!「NEWT Product Advent Calendar 2024」9日目は、令和トラベルで海外旅行アプリ『NEWT(ニュート)』のAndroidを開発しているともながが、エンジニアリングオフィスのメンバーからバトンをもらい、Jetpack Navigation Compose 2.8.0で導入されたType safeナビゲーションへの移行について紹介します。ぜひ最後までご覧ください!
はじめに
Jetpack Navigation 2.8.0 で、type safeなナビゲーションができるようになりました。
「NEWT Androidでもさっそく使いたい!」ということで、実際にNEWT Androidで使用している既存のナビゲーション処理を移行してみました。
これまでとの違いを簡単に説明すると、Navigation 2.8.0ではSerializableな型であればなんでもナビゲーションのrouteとして利用することができるようになりました。定義したクラスにパラメータを持たせることで、destination間のデータの受け渡しを実現します。
モデル定義
まずはSerializableなクラスを作っていきます。これまではrouteをStringで定義していましたがこの部分はまるっとなくなります。
@Serializable
アノテーションをつけていきます。後述しますがReservationでは画面にパラメーターを渡したいため、引数としてreservationId
を取れるようにしておきます。Before
sealed class Screen( val route: String ) { object ReservationList : Screen("reservations") object Reservation : Screen("reservations/%s") }
After
sealed interface ScreenRoute { @Serializable data object ReservationList : ScreenRoute @Serializable data class Reservation(val reservationId: Int? = null) : ScreenRoute }
ナビゲーションの置き換え
NavControllerの各APIをtype safety版に置き換えていきます。
Before
fun navigateToReservationList() { navController.navigate(Screen.ReservationList.route) }
After
fun navigateToReservationList() { navController.navigate(ScreenRoute.ReservationList) }
これまでは、routeのパスに含めるかたちで渡していた
reservationId
をReservationの引数として渡せるようになり、シンプルでわかりやすくなりました👏Before
fun navigateToReservation(reservationId: Int?) { val reservationRoute = if (reservationId == null) { Screen.Reservation.route } else { Screen.Reservation.route.format(reservationId) } navController.navigate(reservationRoute) }
After
fun navigateToReservation(reservationId: Int?) { navController.navigate(ScreenRoute.Reservation(reservationId)) }
NavGraph
これまでのナビゲーショングラフでは、routeでの
reservationId
プレースホルダーの指定とargumentsの定義を行っていましたが、新しいAPIではその部分もまるっと無くなり、routeの型を指定すれば終わりです。Before
fun NavGraphBuilder.reservationGraph() { navigation(startDestination = Screen.ReservationList.route, route = Graph.Reservation.route) { composable(Screen.ReservationList.route) { ReservationListScreen() } composable( route = Screen.Reservation.route.format("{reservationId}"), arguments = listOf( navArgument("reservationId") { type = NavType.IntType nullable = true } ), ) { ReservationScreen() } } }
After
fun NavGraphBuilder.reservationGraph() { navigation<GraphRoute.Reservation>(startDestination = ScreenRoute.ReservationList) { composable<ScreenRoute.ReservationList> { ReservationListScreen() } composable<ScreenRoute.Reservation> { ReservationScreen() } } }
超シンプルですね〜🙌
パラメータの受け取り
NEWT Androidでは、ViewModelの
SavedStateHandle
からkeyを指定してパラメータを取得していましたが、新しいAPIでは toRoute()
を使うことで直接routeに指定したオブジェクトを取得でき、そこからパラメータへアクセスできます。key名や型を気にする必要がなくなって嬉しいです🎉Before
val reservationId = savedStateHandle.get<Int>("reservationId")
After
val reservationRoute: Route.Reservation = savedStateHandle.toRoute() val reservationId = reservationRoute.reservationId
以上が基本的なtype safe ナビゲーションのやりかたなのですが、NEWT Androidでは他にも考慮が必要な点があったので続けて紹介していきます。
routeのString値で判定していた箇所の置き換え
プロジェクト内で、destination.routeが特定の値だった場合の分岐処理が何箇所かあったので、こちらも新しいAPIに合わせました。新しいAPIでは、destination.routeには指定したモデルのフルパス + パラメータが入るようになります。
- パラメータなしの場合
"net.newt.ScreenRoute.ReservationList”
- パラメータありの場合
"net.newt.ScreenRoute.Reservation?reservationId={reservationId}"
qualifiedName
を用いてモデルのフルパスの取得し、判定に使いました。sealed interface ScreenRoute { val path get() = this::class.qualifiedName … }
hasRoute 判定
一部のdestinationでは、そのrouteがバックスタック内にあればpop, そうでなければnavigate するという処理を行っていました。こちらは
hasRoute(KClass<T>)
を使って実現することができます。fun navigateToMyPage() { if(hasRouteInBackStack(ScreenRoute.MyPage)) { navController.popBackStack(ScreenRoute.MyPage, false) } else { navController.navigate(ScreenRoute.MyPage) } } @SuppressLint("RestrictedApi") fun hasRouteInBackStack(route: ScreenRoute): Boolean { return navController.currentBackStack.value.any { it.destination.hasRoute(route::class) } }
ちなみに、Android StudioのK2モードを有効にしていると、
hasRoute(KClass<T>)
の呼び出しでRestrictedApiのワーニングが出てしまう不具合があります。
既存のライブラリメソッドhasRoute(route: String, arguments: Bundle?)
が参照されてしまうようで、本稿執筆時点で解消されていません。当面は
@SuppressLint("RestrictedApi")
を付けるなどの対応が必要そうです。まとめ
従来の型安全ではないナビゲーションAPIでは、NavGraphが肥大化しやすく可読性が低下したり、文字列ベースによるタイプミスや変更の反映もれなども起きやすく、開発時に地味に時間を取られるポイントになっていました。
Type safeナビゲーションを採用することで、型の整合性エラーやタイプミスによるエラーの心配もなくなったり、IDEの補完機能も活用できたりと、実装がスムーズになるのを実感しました!
NEWT Androidではまだ一部の画面しか移行できていないのですが、順次対応を進めてよりこのメリットを活かせるようにしたいです。以上、Jetpack Navigation Compose 2.8 の Type safe ナビゲーションの紹介でした。
参考ページ
「NEWT Tech Talk」のお知らせ
令和トラベルでは、技術的な知識や知見・成果を共有するLT会を毎月実施しています。発表テーマや令和トラベルに興味をお持ちいただいた方は、誰でも気軽に参加いただけます。
【特別編】After Startup CTO of the year 2024 開催決定🎊
この度、「Startup CTO of the year 2024」にて弊社VPoE麻柄が栄えある最優秀賞を受賞しました。6分間のピッチ『事業成長を導く技術戦略』では語り尽くせなかった、プロダクト開発や組織づくりの裏側について、EMのmiisan・Senior Engineerの飯沼を交えて、パネルディスカッション形式でさらに深ぼっていきます!
今後も定期的に、令和トラベルの技術や組織に関する情報を発信するイベントを開催予定です。connpass のメンバー登録をしていただき、最新情報をお見逃しなく!
令和トラベルでは一緒に働く仲間を募集しています
この記事を読んで会社やプロダクトについて興味をお持ちいただけましたら、ご連絡お待ちしています!フランクに話だけでも聞きたいという方は、カジュアル面談も実施できますので、お気軽にお声がけください。
今年最後の特大セールも開催中!
NEWTでは、12/26 お昼11:59まで、2024年最後のおトクなセールを開催中です!
📣宣伝
次回の「NEWT Product Advent Calendar 2024」Day10は、Backend エンジニアのSembaが担当します。12月5日に開催したハッカソン形式の開発合宿『Bet on Future Day』について紹介する予定です。次のブログもお楽しみに!