📱
[Android] Jetpack Compose Navigation Type Safe 詳細
Jetpack Compose Navigationが2.8.0から遷移する時に渡すパラメータが型安全になった。
ルート定義
ルート定義はシリアル化可能なクラスである必要があるのでKotlin Serializationプラグインが必須
遷移時に渡したい引数はデータクラスのプロパティ(下層にも@Serializableをつけ忘れないこと)
デフォルト引数の場合はクエリ、そうでない場合はパスになる
@Serializable
data class FullName(
val firstName: String,
val middleName: String? = null,
val lastName: String,
)
// 生成されるパス
example://full_name/{first_name}/{last_name}?middle_name={middle_name}
@Serializable
sealed class AppDestination {
@Serializable
data object Splash : AppDestination()
@Serializable
data object ScreenA : AppDestination()
@Serializable
data class ScreenB(
val id: String,
) : AppDestination()
@Serializable
data class ScreenC(
val model: CustomModel,
) : AppDestination()
}
ナビゲータ定義
ナビゲータ定義は必須ではないが、あれば遷移時の挙動を制御できるので便利
今まではpathを直接書く必要があったが、navigationにルート定義したデータクラスを入れるだけでよくなった
class AppNavigator(
val navController: NavHostController,
) {
val navigateToScreenA: () -> Unit = {
navController.navigate(AppDestination.ScreenA) {
popUpTo(navController.graph.id) {
inclusive = true
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
val navigateToScreenB: (String) -> Unit = {
navController.navigate(AppDestination.ScreenB(it))
}
}
ナビゲーション定義
引数なし
composableに型パラメータを指定するだけ
@Composable
fun AppNavGraph(
modifier: Modifier = Modifier,
navigator: AppNavigator,
) {
NavHost(
navController = navigator.navController,
startDestination = AppDestination.Splash,
modifier = modifier,
enterTransition = {
EnterTransition.None
},
exitTransition = {
ExitTransition.None
},
) {
composable<AppDestination.Splash> {
SplashScreen()
}
composable<AppDestination.ScreenA> { backStackEntry ->
ScreenA()
}
}
引数あり
composableの型パラメータを指定する
backStackEntry#toRouteで取得
composable<AppDestination.ScreenB> { backStackEntry ->
val screenB = backStackEntry.toRoute()
ScreenB(screenB)
}
カスタム NavType を定義
データクラスで定義したプロパティの型全てにJetpack Compose Navigationは対応しているわけではない
対応している型は以下
まず、カスタムクラスに@Serializableをつける(下層まで)
@Serializable
data class CustomModel(
id: String = "",
data: Data = Data()
)
カスタムクラスをシリアライズ・デシリアイズするためのインライン関数を作る
serializeAsValue
シリアライズされるものによっては、JSONデータに、スラッシュ(/)、カッコ、引用符、スペースなどが含まれることがあり、それがパスと誤認識されるのでUri.encodeしてエスケープする
/**
* シリアライズ可能な型のためのNavTypeを生成するインライン関数
* @param T シリアライズしたい型(@Serializableアノテーションが付いている必要があります)
* @param isNullableAllowed この型がnullを許容するかどうか(デフォルトはfalse)
* @param json シリアライズ/デシリアライズに使用するKotlinx.serializationのJsonインスタンス
* @return 指定された型T用にカスタマイズされたNavType
*/
inline fun <reified T : Any> serializableType(
isNullableAllowed: Boolean = false,
json: Json = Json,
) = object : NavType<T>(isNullableAllowed = isNullableAllowed) {
override fun get(bundle: Bundle, key: String) = bundle.getString(key)?.let { json.decodeFromString<T>(it) }
override fun parseValue(value: String): T = json.decodeFromString(value)
override fun serializeAsValue(value: T): String = Uri.encode(json.encodeToString(value))
override fun put(bundle: Bundle, key: String, value: T) = bundle.putString(key, json.encodeToString(value))
}
navigationGraphでtypeMapを指定する
あとは同じ
composable<AppDestinations.ScreenC>(
typeMap = mapOf(typeOf<CustomModel>() to serializableType<CustomModel>()),
) { backStackEntry ->
val screenC: AppDestination.ScreenC = backStackEntry.toRoute()
ScreenC(screenC)
}
ディープリンク
コールドスタート時から起動したい場合はAndroidManifestの設定を行う
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="fogefoge"
android:host="webview"
android:pathPattern="/.*" />
</intent-filter>
受け取る形式のパスを定義する以下の場合はfogefoge://foge
クエリパラメータが存在する場合でもパスのみで良い
object DeepLinkDestinations {
private const val CUSTOM_SCHEME = "fogefoge://"
object ScreenA {
private const val HOST = "webview"
const val CUSTOM_PATH = "$CUSTOM_SCHEME$HOST"
}
}
ルート定義
クエリパラメータはデフォルト引数で定義する
@Serializable
data class ScreenD(
val webview: String,
val a: String = ""
)
ナビゲーション定義
composable<ScreenD>(
deepLinks = listOf(
navDeepLink<ScreenA>(
basePath = DeepLinkDestinations.ScreenA.CUSTOM_PATH,
),
),
) { backStackEntry ->
val screenD: ScreenD = backStackEntry.toRoute()
// 画面UI
ScreenA(screenD)
}
以下で確認
adb shell am start -W -a android.intent.action.VIEW -d "fogefoge://webview/https://www.google.com/?a=テスト"
参考
Discussion