アプリの基礎

Jetpack Navigation3 で型安全なディープリンクを構築する

スピーカー

  • Clara Fok

    ソフトウェア エンジニア

  • Ian Lake

    エンジニアリング マネージャー

概要

講演について

モジュール式の Android アプリでディープリンクを管理すると、ルートが破損したり、実行時に予期しない動作が発生したりすることがあります。このセッションでは、Compose 向けの Jetpack Navigation3 の新しいタイプセーフなディープリンク アーキテクチャについて説明し、堅牢でスケーラブルなルーティング システムを構築する方法を紹介します。すぐに使えるディープリンク マッチング API の実装方法、複雑な型安全引数用のカスタム シリアライザーの作成方法、ユーザー ナビゲーションを予測可能に保つ動的な階層型バックスタックの構築方法について説明します。

概要

  • Navigation 3 ディープリンク API は、受信したディープリンク リクエストをナビゲーション キーに変換して、初期バックスタックを決定します。KMP 互換の DeepLinkRequest クラスは、Android Intent コンストラクタを含む入力をカプセル化します。
  • UriDeepLinkMatcher は、Kotlin Serialization を使用して URI パターンを照合し、引数をデシリアライズして型安全な NavKey プロパティに変換します。プリミティブ型は自動的に機能し、値を解析できない場合は一致しません。
  • デベロッパーは、DeepLinkSerializer を拡張するか、matchArguments をオーバーライドして、複雑なディープリンク文字列を処理できます。UriDeepLinkMatcher をサブクラス化すると、Kotlin Serialization が実行される前に、未加工の URI プレースホルダを前処理できます。
  • マッチャーで withBackStack を呼び出すと、最適な一致のために合成バックスタックが遅延作成されます。これにより、他の有効な一致ごとに不要なバックスタックを計算することなく、適切なシステムバック動作が保証されます。

リソース

Gemini Notebook

トピック

  • jetpack-navigation-3
  • ディープリンク
  • kotlin-serialization
  • kotlin-multiplatform
  • android-architecture

文字起こし

概要と Navigation 3 のレシピ

  • こんにちは。Jetpack アーキテクチャ チームのエンジニアリング マネージャー、Ian Lake です。

  • 私は Clara Fok です。Nav 3 の技術責任者を務めています。

  • 本日は、私がとても気に入っている機能、ナビゲーション、ディープリンクについてお話しします。

  • 実際、私はこの機能がとても気に入ったので、Nav3 の 1.2 リリースの一環として、Nav2 と Nav3 の両方に実装しました。

  • 実際には 3 回行ったと思います。Nav 3 のレシピでも実装を行いました。

  • はい、正解は ○ です。

  • API のイテレーションを何度も行い、Nav3 の Compose ファーストのアプローチに馴染むようにしました。

  • その多くは、レシピを重視することから始まりました。

  • はい。Navigation 3 の主な目標の一つは、開発者の邪魔をしないことでした。

  • 特に野心的な機能を構築する場合、実際にそれを可能にするために Google からの機能リクエストは必要ありません。

  • 「どうすればいいのか」という質問を受けたとき、

  • 「この問題にどう取り組めばよいか?」

  • カスタム API を最初から構築するのではなく、レシピを構築します。レシピとは、Navigation 3 に変更を加えることなく、その機能をアプリに追加する方法を説明するガイドです。

  • この「言葉ではなく、行動で示す」というスタイルが、GitHub での Nav3 レシピ リポジトリの作成につながりました。

  • 現在、複数のバックスタックやボトムシートなど、一般的なユースケースに対応する 35 以上のレシピが用意されており、アプリに実装するために必要なコードがすべて提供されています。

  • つまり、2025 年 11 月の Nav3 の 1.0 リリースの時点で、ディープリンクを実装するためのレシピがすでに用意されていました。これは、多くのデベロッパーにとってディープリンクが重要であることを認識していたためです。

  • しかし、レシピにアプリにコピーして貼り付ける必要があるヘルパー メソッドが多数必要であることがわかった場合は、卒業と呼ばれるプロセスを検討します。このプロセスでは、これらのヘルパーを取り上げて、「これらが Navigation 3 自体の一部であった場合、実際にはどうなるか」を検討します。

  • これにより、すでにそのコードをコピーして貼り付けたデベロッパーは問題ありません。

  • そのコードはそのままにしておいてください。

  • カスタム ソリューションが機能している場合は、それで問題ありません。

  • 強制移行はありません。

  • しかし、新しい問題や新しい要件に直面した場合は、検証済みの簡単なソリューションを入手して、ナビゲーションの一部として直接使用できます。

  • 個人的に最も気に入っているのは、ヘルパーを追加した後にレシピを更新し、コードを削除するという、ドーパミンが放出されるような爽快感を味わえることです。

  • そうすることで、その問題の推奨されるアプローチのレシピを見ている人は誰でも、最新のソリューションを入手できます。

  • もちろん、後から振り返れば何とでも言えます。この移行プロセスは、レシピで作成したヘルパーをじっくり見直して、「本当にこれが最適な API なのか?」と自問する機会になります。

  • この場合、ライブラリに移行する際に、それらの機能を改善する機会が得られます。

  • ディープリンクについても、まさにそのとおりでした。

ディープリンク API の設計と URI パターン マッチング

  • Ian さん、ありがとうございました。

  • そのため、コンセプトとしては、レシピと最終的な API の両方が非常に似ています。これは、Nav 3 ではバックスタックを所有しているためです。

  • つまり、ディープリンク API の焦点は、最初のバックスタックがどうあるべきか、具体的には、ユーザーがディープリンクをクリックしてアプリに入ったときに表示される画面を特定することです。

  • しかし、ユーザーがそのウェブ URI をクリックすると、実際には何が起こるでしょうか。

  • 現在、Nav3 を使用するかどうかにかかわらず、Android フレームワークでは、アプリがサポートするディープリンクごとにインテント フィルタを宣言する必要があります。

  • 興味深いのは、ディープリンクによって Activity が開始された後の動作です。

  • ここで、入力はアプリが起動されたインテント、つまり起動されたアクティビティです。

  • ただし、ディープリンクにはさまざまな形式があります。

  • たとえば、テスト用に手動で作成する場合や、Kotlin マルチプラットフォームのサポートを利用する場合などです。

  • そこで、ディープリンク入力を KMP 互換の DeepLinkRequest クラスにカプセル化しました。

  • Android では、Android インテントを受け取る DeepLinkRequest コンストラクタがあります。

  • 次に、リクエストを受け取ったときに、どの NavKey をバックスタックに配置する必要があるかを判断する方法を考えます。

  • ここで DeepLinkMatcher クラスの出番です。

  • リクエストを受け取り、一致に関連付けられた NavKey を含む MatchResult を出力します。

  • 特にウェブ URI の場合、指定されたリクエストを MatchResult に変換するために必要なすべてのロジックをカプセル化する UriDeepLinkMatcher があります。

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • UriDeepLinkMatcher が受け取る最初のパラメータは URI パターンになりました。

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • たとえば、ADS アプリを構築していて、ホーム画面にディープリンクを追加したいとします。

  • この例では、パターンは完全な URI であり、HTTP スキーマを省略して HTTP と HTTPS の両方に自動的に一致させることができます。

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • しかし、特定の広告セッションにディープリンクする場合は、そのディープリンクに、抽出する必要がある情報(セッション ID)が含まれるようになります。

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • この場合、パターンでは、ディープリンクから抽出する必要がある情報のプレースホルダとして中かっこを使用します。

  • そして、あらゆる種類の入力をクラス インスタンスに変換する便利なライブラリがすでに存在することがわかりました。それが Kotlin Serialization です。

  • そのため、UriDeepLinkMatcher は Kotlin デコーダを利用して入力 URI をデシリアライズし、NavKey インスタンスを出力します。

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • このデコーダは、どのクラスを処理するかを認識する必要があるため、NavKey のシリアライザーをマッチャーに渡す必要もあります。

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • また、デコーダがプレースホルダをキーのプロパティに適切に一致させられるように、プレースホルダの名前はプロパティ名と一致している必要があります。

  • この例では、プロパティ名はセッションの ID パラメータと一致する ID です。

  • UriDeepLinkMatcher が用意できたので、受信したディープリンクを使用して DeepLinkRequest を作成する必要があります。

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
    
    val request = DeepLinkRequest(
      uri = DeepLinkUri("http://ads.android.com/session/abcd")
    )
    
    val matchResult: UriMatchResult<Session>? = matcher.match(request)
    
    val backStack = listOf(matchResult?.key ?: Home)
  • 次に、match 関数を呼び出してリクエストを渡すことで、マッチングが行われます。

  • これにより、NavKey を含む、null 値を許容する MatchResult が返されます。この例では、セッション キーになります。結果が null の場合は、デフォルトでホーム画面に移動します。

  • 複数のディープリンクは、複数のマッチャー(ディープリンクごとに 1 つのマッチャー)として記述する必要があります。

  • たとえば、この例では、セッション キーに 2 つの URI パターンがあります。1 つは以前のニュースレター パターン用、もう 1 つは通常のウェブ URI 用です。

  • また、ADS ホーム画面へのディープリンクもあるため、3 つ目のマッチャーを追加します。

  • すべてのマッチャーは、反復処理する同じリストに追加されます。

  • そこで、この match メソッドを、リスト内のすべてのマッチャーをループ処理して、それぞれに対して match を呼び出し、リクエストに一致するすべてのマッチャーを見つけるように変更します。

  • これを行うには、mapNotNull を使用して一致しないものを除外し、有効な一致のみのリストを取得します。

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • ほとんどの場合、リスト内の一致する null 以外の値は 1 つだけですが、有効な一致が複数存在する場合もあります。

  • たとえば、同じセッション URI パターンを再利用する特別な基調講演画面を導入するとします。ただし、ID プレースホルダではなく、静的な基調講演パスを使用します。

  • この場合、同じリクエストが、ID が keynote と等しいセッションキーと keynote NavKey の両方のマッチャーに一致する可能性があります。

  • MatchResult は Comparable を実装しているため、max 関数を使用してリスト内の有効な一致をすべて比較することで、最適な一致を見つけることができます。実際には、リクエストに一致するディープリンクがない場合を処理するために maxOrNull を使用します。

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
    
    // compare matches
    val backStack = listOf(
      matchResults.maxOrNull()?.key ?: Home
    )
  • マッチャーの静的リストは、ライトニング トークでは理にかなっているかもしれませんが、画面が複数のモジュールに分散される可能性があるのと同様に、同じモジュールが DI システムを介してマッチャーのグローバル リストに貢献することが期待されます。

    // Feature A Module
    @ContributesTo(ActivityScope::class)
    @BindingContainer
    object FeatureAModule {
        @IntoSet
        @Provides
        fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
          UriDeepLinkMatcher(
            uriPattern = DeepLinkUri("ads.android.com/featureA"),
            serializer<FeatureBKey>(),
          )
    }
    
    // in MainActivity
    @Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
  • このアプローチを使用して、レシピ リポジトリの Hilt、Metro、Koin のレシピが更新され、ディープリンクが含まれるようになりました。

タイプセーフな逆シリアル化とカスタム シリアライザー

  • クララが言ったように、UriDeepLinkMatcher は Kotlin Serialization を使用してディープリンク リクエストからフィールドを抽出し、先ほど紹介したセッション クラスのようなタイプセーフ クラスに変換します。

  • ここに書かれているように、これは非常に単純な変換です。

  • URI は文字列です。

  • フィールドは文字列です。

  • 簡単ですね。

  • しかし、文字列プロパティだけでは不十分であることはわかっていました。

  • この点において、Kotlin Serialization はすでに強固な基盤を提供しています。

  • ブール値、整数、浮動小数点数、列挙型などのすべてのプリミティブ型は、すぐに使用できます。ディープリンク値を特定の型として解析できない場合は、自動的に一致に失敗し、null が返されます。

  • たとえば、セッションの ID パラメータが文字列ではなく整数である場合、整数に解析できない文字列を渡すと、一致は失敗します。

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • 同様に、ウェブ URL に添付された分析データなどのオプションの引数が必要な場合は、デフォルト値を持つキーのプロパティにすることができます。

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • ここでは、ウェブ URL の構造を、アプリ内のディープリンクの一致を表す型安全なクラスまたはクラスから切り離すことに努めています。ウェブ URL の構造は、別のチームが所有している可能性があります。

  • なぜなら、皆さんの中には、非常にひどいディープリンクを扱っている方もいるからです。

    // "id={id}&base64={placeholderImage}"
    @Serializable data class Session(
      val id: String,
      val placeholderImage: Bitmap? = null
    ) : NavKey
    
    // "jsonBlob={user}"
    @Serializable data class Profile(
      val user: User,
    ) : NavKey
  • たとえば、ディープリンクに base64 でエンコードされた画像が添付されている場合、

  • たとえば、巨大な JSON blob をアプリのすべての URL に入れることにしたとします。

  • これで、指定されたウェブ URL を変更できない場合でも、そこから情報を抽出する方法ができました。

  • そこで、最初のテクニックとして、DeepLinkSerializer クラスを拡張して、その引数をデシリアライズする方法、つまり String から型安全なプロパティに変換する方法を正確に制御します。

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer は、Kotlin Serialization のヘルパー ラッパーです。この場合、デシリアライザの入力は常にウェブ URL の文字列であるため、シリアライザ クラスの実装が簡素化されます。

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • DeepLinkSerializer を実装するには、3 つのメソッドを記述します。

  • まず、シリアル名を選択します。

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • 名前空間を指定する必要はありませんが、Kotlin Serialization がシリアライザを使用するタイミングを認識できるように、一意である必要があります。

  • 次に、deserialize を実装する必要があります。これは、ウェブ URL から String を渡され、型安全なオブジェクトを構築する役割を担います。

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      override fun deserialize(value: String): Bitmap {
        val imageBytes = Base64.decode(value, Base64.DEFAULT)
       // This better be a small image…
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
      }
    
       ...
    
    }
  • 最後に、逆のアプローチである serialize を実装します。これは、同じキーのクラスが rememberNavBackStack のようなケースで使用される場合に使用されます。

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
    
      ...
    
      override fun serialize(value: Bitmap): String {
        val byteArrayOutputStream = ByteArrayOutputStream()
        // Compress the bitmap into the output stream (e.g., JPEG or PNG)
        value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
        val byteArray = byteArrayOutputStream.toByteArray()
        // Encode the byte array to a Base64 string
        return Base64.encodeToString(byteArray, Base64.DEFAULT)
      }
    }
  • 次に、カスタム シリアライザーを、少し特殊なクラスを持つプロパティに適用します。残りの処理はウェブ UriDeepLinkMatcher が行います。

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
    
    @Serializable data class Session(
      val id: String,
      @Serializable(BitmapSerializer::class)
      val placeholderImage: Bitmap? = null
    ) : NavKey

カスタム マッチャーを使用した URL 引数の前処理

  • しかし、その Base64 文字列やウェブ URL に添付された JSON から、ほんの一部のデータだけを取得したい場合はどうすればよいでしょうか。

  • その場合は、Kotlin Serialization に渡す前にウェブ URL を前処理することをおすすめします。

  • このようなケースに対応するには、UriDeepLinkMatcher を拡張して matchArguments メソッドをオーバーライドします。

  • JSON blob で必要なのはセッション ID だけで、残りのデータは不要な場合もあります。

  • そこで、サブクラスを作成して matchArguments をオーバーライドします。

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        ...
    }
  • 入力は、URI パスから追加されたすべてのプレースホルダである未加工の文字列です。

  • これにより、super.matchArguments を呼び出す前に、これらの値を前処理して編集できます。

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        val finalQueryArgs = buildMap {
          putAll(queryArgs)
          val jsonBlob = remove("session")?.firstOrNull()
          if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
        }
        return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
    }
  • これで、簡素化された値を使用できるようになり、Kotlin Serialization を操作する必要がなくなりました。

ディープリンク用の合成バックスタックの作成

  • Ian さん、ありがとうございました。

  • これらの手法を頻繁に使用する必要がないことを願っていますが、Google はマッチング動作を制御する方法について多くのことを検討しました。

  • 違いますね。

  • ディープリンク マッチャーを使用して NavKey を取得する方法がわかりましたが、ナビゲーション キーのみを含むバックスタックでは、最適なユーザー エクスペリエンスを提供できない可能性があります。

  • そのため、戻る動作を適切にするには、合成バックスタックが必要です。

  • 合成部分とは、バックスタックに、ユーザーが手動で移動したことのないキーが含まれているものの、手動で移動した場合にたどるパスであるという事実から生じます。

  • たとえば、まもなく開始されるセッションの通知をクリックすると、ユーザーはそのセッションに直接移動しますが、システム バックをトリガーした場合は、ランチャーに移動する前に HomeScreen に戻るようにしたいでしょう。

  • その HomeScreen は合成バックスタックの一部になります。

  • ディープリンク API では、2 つの相反する要件のバランスを取る必要がありました。

  • 一方、ディープリンクやマッチャーが異なれば、合成バックスタックを構築するためのロジックも異なることが予想されます。

  • たとえば、すべてのディープリンクの前に HomeScreen を追加すると、設定画面内のサブ画面にディープリンクするまでは機能する可能性があります。

  • この場合、その特定のディープリンク用に、合成バックスタックにレイヤを追加する必要があります。

  • 当然ながら、これによりバックスタック ビルダーが各ディープリンク マッチャーに関連付けられます。

  • しかし、その一方で、正しい画面が表示されるまでの 1 ミリ秒ごとに、実際の費用がかかることもわかっています。

  • そのため、一致する結果ごとに合成バックスタックを計算することは避けたいと考えました。

  • 最適な一致のみを求めていました。

  • では、各マッチャーにカスタム ロジックが必要だが、各マッチに対して合成バックスタックを積極的に作成したくない場合は、どうすればよいでしょうか。

  • 何度か失敗を繰り返した後、両方の要件のバランスを取るアプローチを見つけました。

  • どのような種類のマッチャーでも、withBackStack を呼び出して、指定された MatchResult でバックスタックを遅延構築することで、その特定のマッチャーを拡張できます。

    val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, matchResult.key)
    }
    
    val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, SessionList, matchResult.key)
    }
    
    val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
      homeMatcher, sessionMatcher
    )
    
    val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
    val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()
    
    val backStack = bestMatch?.backStack ?: listOf(Home)
  • したがって、複数のマッチャーがある場合、それぞれに独自の withBackStack 呼び出しがあり、特定の NavKey に適したバックスタックが構築されます。

  • 呼び出す各マッチャーが withBackStack を使用すると、バックスタック マッチャーのリストが返され、最もクリーンなコードが得られます。

  • 最適な一致が見つかった場合、返される一致結果は BackStackMatchResult です。

  • これは MatchResult のサブクラスで、backStack という新しいプロパティが追加されています。これは、withBackStack ラムダによって生成されたバックスタック全体です。

  • このレイヤ化アプローチにより、マッチャー内のロジックを、withBackStack ラムダ内で使用するカスタム ロジックから切り離すことができました。

ライトニング ラウンド、まとめ、リソース

  • 今回はライトニング トークなので、残念ながらもう時間がありませんが、Navigation 3 のその他の機能については、Navigation 3 のレシピで確認できます。

  • 違いますね。

  • そのため、任意の matcher にフィルタを追加して、マッチング ロジックをショートサーキットし、アクション、MIME タイプ、または独自のカスタム フィルタに基づいて matcher をすばやく除外できます。

  • これは、URI 解析やカスタム シリアル化(Kotlin Serialization)を行う前に発生します。

  • また、NavKey を取得するために解析を行う必要はないが、フィルタを適用したい場合に備えて、StaticKeyDeepLinkMatcher も用意されています。

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • これは、JPEG 画像を開くことができる写真編集アプリなど、アクションまたは MIME タイプのみのディープリンクを処理する場合に最適です。

  • アクションと MIME タイプに関連して、任意のカスタム RequestExtras を DeepLinkRequest にアタッチできます。カスタム フィルタと DeepLinkRequest サブクラスは、このカスタム をロジックの一部として使用できます。

  • これにより、A/B テスト フレームワークや外部シグナルを組み込むことができます。

  • また、エクストラ用の型安全な API も用意されています。

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • このロジックのほぼすべてが Nav3 の共通 KMP コードに含まれているため、すべてのマッチャーとバックスタック ロジックをプラットフォーム間で共有できます。

  • Navigation 3 がディープリンクをどのようにサポートしているかを詳しく知りたい場合は、Nav3 のレシピをご覧ください。

  • はい。また、ディープリンク API を DI レシピと、ヒーローアプリである NowInAndroid にも統合しました。

  • ライトニング トークにご参加いただきありがとうございました。

  • ありがとうございました

コード

コード スニペット

「ディープリンク API の設計と URI パターン マッチング」の 4 分 41 秒の Kotlin スニペット
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
「ディープリンク API の設計と URI パターン マッチング」の 4 分 53 秒の Kotlin スニペット
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
「ディープリンク API の設計と URI パターン マッチング」の 05:04 の Kotlin スニペット
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
「ディープリンク API の設計と URI パターン マッチング」の 05:12 の Kotlin スニペット
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
「ディープリンク API の設計と URI パターン マッチング」の 5 分 39 秒の Kotlin スニペット
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
「ディープリンク API の設計と URI パターン マッチング」の 5 分 48 秒の Kotlin スニペット
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
「ディープリンク API の設計と URI パターン マッチング」の 06:12 の Kotlin スニペット
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)

val request = DeepLinkRequest(
  uri = DeepLinkUri("http://ads.android.com/session/abcd")
)

val matchResult: UriMatchResult<Session>? = matcher.match(request)

val backStack = listOf(matchResult?.key ?: Home)
「ディープリンク API の設計と URI パターン マッチング」の 07:11 の Kotlin スニペット
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
「ディープリンク API の設計と URI パターン マッチング」の 7 分 48 秒の Kotlin スニペット
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}

// compare matches
val backStack = listOf(
  matchResults.maxOrNull()?.key ?: Home
)
「ディープリンク API の設計と URI パターン マッチング」の 8:04 の Kotlin スニペット
// Feature A Module
@ContributesTo(ActivityScope::class)
@BindingContainer
object FeatureAModule {
    @IntoSet
    @Provides
    fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
      UriDeepLinkMatcher(
        uriPattern = DeepLinkUri("ads.android.com/featureA"),
        serializer<FeatureBKey>(),
      )
}

// in MainActivity
@Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
「型安全な逆シリアル化とカスタム シリアライザー」の 9 分 15 秒の Kotlin スニペット
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
「型安全なデシリアル化とカスタム シリアライザー」の 9 分 27 秒の Kotlin スニペット
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
「型安全なデシリアル化とカスタム シリアライザー」の 09:53 の Kotlin スニペット
// "id={id}&base64={placeholderImage}"
@Serializable data class Session(
  val id: String,
  val placeholderImage: Bitmap? = null
) : NavKey

// "jsonBlob={user}"
@Serializable data class Profile(
  val user: User,
) : NavKey
「タイプセーフなデシリアル化とカスタム シリアライザー」の 10:19 の Kotlin スニペット
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
「型安全なデシリアル化とカスタム シリアライザー」の 10:32 の Kotlin スニペット
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
「型安全なデシリアル化とカスタム シリアライザー」の 10:51 の Kotlin スニペット
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
「型安全な逆シリアル化とカスタム シリアライザー」の 11:01 の Kotlin スニペット
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  override fun deserialize(value: String): Bitmap {
    val imageBytes = Base64.decode(value, Base64.DEFAULT)
   // This better be a small image…
    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
  }

   ...

}
「型安全なデシリアル化とカスタム シリアライザー」の 11:13 の Kotlin スニペット
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {

  ...

  override fun serialize(value: Bitmap): String {
    val byteArrayOutputStream = ByteArrayOutputStream()
    // Compress the bitmap into the output stream (e.g., JPEG or PNG)
    value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
    val byteArray = byteArrayOutputStream.toByteArray()
    // Encode the byte array to a Base64 string
    return Base64.encodeToString(byteArray, Base64.DEFAULT)
  }
}
「タイプセーフなデシリアル化とカスタム シリアライザー」の 11:24 の Kotlin スニペット
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
「Preprocessing URL arguments with custom matchers」の 12:09 の Kotlin スニペット
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    ...
}
「カスタム マッチャーを使用した URL 引数の前処理」の 12:21 の Kotlin スニペット
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    val finalQueryArgs = buildMap {
      putAll(queryArgs)
      val jsonBlob = remove("session")?.firstOrNull()
      if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
    }
    return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}
「ディープリンク用の合成バックスタックの構築」の 14:31 の Kotlin スニペット
val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, matchResult.key)
}

val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, SessionList, matchResult.key)
}

val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
  homeMatcher, sessionMatcher
)

val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()

val backStack = bestMatch?.backStack ?: listOf(Home)
「ライトニング ラウンド、まとめ、リソース」の 15:52 の Kotlin スニペット
@Serializable object ImagesKey: NavKey

// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
「ライトニング ラウンド、まとめ、リソース」の 16:24 にある Kotlin スニペット
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}