Type SafeなAPIを利用したNavigation ComposeでのDeepLinkの実装

以下の記事を読まれた方は多いかと思いますが、Navigation 2.8.0-alpha08からNavigation Composeで型安全なAPIが利用可能になっています。stable版が待ち遠しいですね。

medium.com


上記の記事の最後の方で、DeepLinkについて少しだけ触れられています。しかしながら、実装方法に関する具体的な情報が見つけられなかったので、ライブラリのコードを読みながらどのように実装するのか調べてみました。
調査時のライブラリのバージョンはNavigation 2.8.0-beta05です。

Besides support for all of the Kotlin DSL builders we support (...), it also includes other APIs you might find interesting like the navDeepLink API that takes a Serializable class and a prefix that allows you to easily connect external links to the same type safe APIs.


前提知識: Type SafeなRouteの使い方

詳細については上記の記事や公式ドキュメントをご参照いただければと思いますが、以下のことを押さえておけばある程度問題ないかと思います。
※すでに利用経験のある方は、このセクションをスキップしていただいて問題ありません。

自前のデータ型を使う場合の実装は以下のようになります。

/**
 * 自前のデータ型の定義
 * Route classに渡すため`@Serializable`も必要になる
 */
@Parcelize
@Serializable
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>,
): Parcelable

/** 
 * 自前のデータ型を引数に持つRoute
 */
@Serializable
data class Search(
  val parameters: SearchParameters,
)

/** 
 * Custom Nav Typeを簡単に生成するための汎用関数
 */
@OptIn(InternalSerializationApi::class)
inline fun <reified T : Parcelable> createCustomNavType(
  isNullableAllowed: Boolean = false,
) = object : NavType<T>(isNullableAllowed) {
  override fun get(bundle: Bundle, key: String): T? {
    return BundleCompat.getParcelable(bundle, key, T::class.java)
  }

  override fun put(bundle: Bundle, key: String, value: T) {
    bundle.putParcelable(key, value)
  }

  /**
   * 親クラスに以下の説明があるので、このメソッドもoverrideする必要があることに注意する
   * This method can be override for custom serialization implementation on types such custom NavType classes.
   */
  override fun serializeAsValue(value: T): String {
    return Json.encodeToString(T::class.serializer(), value)
  }

  override fun parseValue(value: String): T {
    return Json.decodeFromString(T::class.serializer(), value)
  }
}

/** 
 * 自前のデータ型に対応するCustom Nav Type
 */
val SearchParametersType = createCustomNavType<SearchParameters>()

/** 
 * `composable()`の`typeMap`に上記で定義したCustom Nav Typeを渡す
 */
composable<Search>(
  typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType),
) {...}


本題: DeepLinkの実装におけるType SafeなRouteの使い方

以下のFullName Routeを例として、DeepLinkの実装におけるType SafeなRouteの使い方を見ていきます。

@Serializable
data class FullName(
    val firstName: String,
    val middleName: String? = null,
    val lastName: String,
)


Type SafeなnavDeepLink() APIを見たところ、以下の情報が最低限必要でした。

  • T: 引数を抽出するRouteの型情報
  • basePath: 引数を追加するベースとなるURI
  • (typeMap: 自前のデータ型の型情報とCustom Nav Typeのマッピング)
public inline fun <reified T : Any> navDeepLink(
    basePath: String,
    typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
    noinline deepLinkBuilder: NavDeepLinkDslBuilder.() -> Unit = {}
): NavDeepLink = navDeepLink(basePath, T::class, typeMap, deepLinkBuilder)


FullName Routeを使って以下のようにDeepLinkを実装すると、マッチングに使われるURI Patternはexample://full_name/{firstName}/{lastName}?middleName={middleName}となりました。

composable<FullName>(
  deepLinks = listOf(
    navDeepLink<FullName>(
      basePath = "example://full_name",
    ),
  )
) {...}


このことから、navDeepLink() APIに渡した情報に基づいて、以下のルールでURI Patternが生成されることが分かります*1

  • デフォルト値を持たないプロパティに対応する引数の情報はpathに追加され、デフォルト値を持つプロパティに対応する引数の情報はqueryに追加される
    • {firstName}{lastName}はpathに追加され、middleName={middleName}はqueryに追加される
  • pathおよびqueryに引数の情報が追加される順番は、Route classに宣言したプロパティの順番に一致する
  • placeholderには各プロパティのプロパティ名が使われる


最後の「placeholderには各プロパティのプロパティ名が使われる」は移行するときに困るケースがありそうですが、Kotlin Serializationの@SerialNameを使うことで変更可能でした。
例えば、以下のように修正すると、マッチングに使われるURI Patternはexample://full_name/{first_name}/{last_name}?middle_name={middle_name}となりました。

@Serializable
data class FullName(
    @SerialName("first_name")
    val firstName: String,
    @SerialName("middle_name")
    val middleName: String? = null,
    @SerialName("last_name")
    val lastName: String,
)


余談:気になったこと

上記のようなシンプルなケースであればスムーズに移行できそうではあったのですが、以下のようなケースではスムーズに移行することは難しいかもしれません。少しずつ知見が集まることを期待しつつ、気になったことをメモ程度に残しておきます。

placeholderのさらに後ろに固定のpathが必要なケース

example://full_name/{first_name}/{last_name}/editのようなDeepLinkがあった場合、今回の実装では生成できなさそうだと思いました。このようなケースが実際にあるのかは分からないですが…

Routeが自前のデータ型を引数に持っているケース

Routeが自前のデータ型を引数に持っているケースでは、DeepLinkは機能するものの、JSONデータをエンコードするなどの操作が必要になるため、あまり実用的ではないと感じました。
例えば、FullName Routeに以下のデータ型を渡した場合、example://full_name/{parameters}{parameters}部分にエンコードしたJSONデータを渡せば遷移できます。ですが、この場合は最初にあったようにフラットな引数に置き換えていくのが良いかと思います。

/*
 * Custom Nav Typeの実装も必要ですが、ここでは割愛します
 */
@Parcelize
@Serializable
data class FullNameParameters(
    @SerialName("first_name")
    val firstName: String,
    @SerialName("middle_name")
    val middleName: String? = null,
    @SerialName("last_name")
    val lastName: String,
) : Parcelable

@Serializable
data class FullName(
    val parameters: FullNameParameters,
)

*1:本記事の説明では、CollectionNavTypeのケースが考慮されていません。ただ、私自身はそのケースの検証を行わなかったため、より詳細な情報が知りたい方はNavDeepLink.Builder#setUriPattern()のKDocRouteBuilder#computeParamType()の実装などもご参照ください

  翻译: