소프트웨어 엔지니어
모듈식 Android 앱 전반에서 딥 링크를 관리하면 경로가 손상되고 런타임에 예기치 않은 문제가 발생하기 쉽습니다.이 세션에서는 Compose를 위한 Jetpack Navigation3의 새로운 유형 안전 딥 링크 아키텍처를 살펴보고, 이를 통해 견고하고 확장 가능한 라우팅 시스템을 빌드하는 방법을 알아봅니다. 기본 제공 딥 링크 일치 API를 구현하고, 복잡한 유형 안전 인수를 위한 맞춤 직렬 변환기를 작성하고, 사용자 탐색을 예측 가능하게 유지하는 동적 계층 구조 백 스택을 구성하는 방법을 살펴보겠습니다.
DeepLinkRequest 클래스는 Android 인텐트 생성자를 포함하여 입력을 캡슐화합니다.UriDeepLinkMatcher는 Kotlin 직렬화를 사용하여 URI 패턴을 일치시키고 인수를 유형 안전 NavKey 속성으로 역직렬화합니다. 기본 유형은 자동으로 작동하며 값을 파싱할 수 없는 경우 일치가 실패합니다.DeepLinkSerializer를 확장하거나 matchArguments를 재정의하여 복잡한 딥 링크 문자열을 처리할 수 있습니다. UriDeepLinkMatcher를 서브클래스로 지정하면 Kotlin 직렬화가 실행되기 전에 원시 URI 자리표시자를 사전 처리할 수 있습니다.withBackStack를 호출하면 최적의 일치를 위해 합성 백 스택이 지연 생성됩니다. 이렇게 하면 다른 모든 유효한 일치 항목에 대해 불필요한 백 스택을 계산하지 않고도 적절한 시스템 뒤로 동작이 보장됩니다.스크립트
안녕하세요. 저는 Jetpack 아키텍처팀의 엔지니어링 관리자인 이안 레이크입니다.
저는 Nav 3의 기술 리드인 클라라 폭입니다.
오늘은 제가 정말 좋아하는 기능인 탐색, 아니 딥 링크에 대해 이야기해 보려고 합니다.
사실 저는 이 기능이 너무 마음에 들어서 Nav3의 1.2 출시의 일환으로 Nav2와 Nav3 모두에 이 기능을 구현했습니다.
Nav 3 레시피에서도 구현을 진행했으니 사실상 세 번 하신 것 같네요.
네, 맞습니다.
Nav3의 Compose 우선 접근 방식에 잘 맞도록 API를 여러 번 반복했습니다.
이러한 변화는 레시피를 강조하면서 시작되었습니다.
예, Navigation 3의 주요 목표 중 하나는 개발자를 방해하지 않는 것이었습니다.
특히 야심 찬 기능을 빌드할 때 실제로 이를 실현하기 위해 Google에 기능 요청을 할 필요가 없습니다.
따라서 "어떻게 해야 하나요?"라는 질문을 받으면
또는 '이 문제에 어떻게 접근해야 할까?'라고 질문할 수 있습니다.
처음부터 맞춤 API를 빌드하는 대신 Navigation 3를 변경하지 않고도 앱에 해당 기능을 추가하는 방법을 설명하는 가이드인 레시피를 빌드합니다.
이러한 '말하지 않고 보여주는' 스타일 덕분에 GitHub에 Nav3 레시피 저장소를 만들 수 있었습니다.
이제 여러 백 스택 및 하단 시트와 같은 일반적인 사용 사례를 위한 35개 이상의 레시피가 제공되며, 앱에 구현하는 데 필요한 모든 코드가 제공됩니다.
즉, 2025년 11월에 출시된 Nav3 1.0 버전에서도 딥 링크를 구현하는 방법이 이미 마련되어 있었습니다. 많은 개발자에게 이 기능이 중요하다는 것을 알고 있었기 때문입니다.
하지만 레시피에 앱에 복사하여 붙여넣어야 하는 도우미 메서드가 많이 필요하다는 것을 알게 되면, 도우미를 가져와서 '이 도우미가 Navigation 3 자체의 일부라면 실제로 어떤 모습일까?'라고 생각하는 '졸업'이라는 프로세스를 고려합니다.
이렇게 하면 이미 해당 코드를 복사하여 붙여넣은 개발자도 문제가 없습니다.
이 코드는 보관하셔도 됩니다.
사용 중인 맞춤 솔루션이 있다면, 잘 작동하는 경우 그대로 사용해도 좋습니다.
강제 이전이 없습니다.
하지만 새로운 문제나 새로운 요구사항이 있는 경우, 탐색의 일부로 직접 사용할 수 있는 검증된 솔루션을 쉽게 얻을 수 있습니다.
하지만 개인적으로 가장 좋은 점은 도우미를 추가한 후 레시피를 업데이트하고 코드를 삭제하는 짜릿한 도파민을 얻는 것입니다.
이렇게 하면 해당 문제에 대한 권장 접근 방식의 레시피를 살펴보는 모든 사용자가 최신 솔루션을 사용할 수 있습니다.
물론 지나고 나면 더 잘 보이는 법입니다. 이 졸업 과정을 통해 레시피에서 빌드한 도우미를 면밀히 살펴보고 '이것이 실제로 가장 적합한 API인가?'라고 자문해 볼 수 있습니다.
이 경우 라이브러리로 이동하면서 이러한 부분을 개선할 수 있는 기회가 생깁니다.
딥 링크도 마찬가지였습니다.
이안, 감사합니다.
따라서 개념적으로 레시피와 최종 API는 매우 유사합니다. Nav 3에서는 개발자가 백 스택을 소유하기 때문입니다.
즉, 딥 링크 API의 초점은 초기 백 스택이 무엇인지, 특히 사용자가 딥 링크를 클릭하여 앱에 들어갈 때 어떤 화면이 표시되는지 파악하는 것입니다.
하지만 사용자가 웹 URI를 클릭하면 실제로 어떤 일이 일어날까요?
이제 Nav3 사용 여부와 관계없이 Android 프레임워크에서는 앱에서 지원하는 각 딥 링크에 대해 인텐트 필터를 선언해야 합니다.
이제 흥미로운 부분은 딥 링크가 Activity를 시작하도록 트리거한 후에 발생하는 일입니다.
여기서 입력은 앱이 시작된 인텐트, 즉 시작된 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 직렬화입니다.
따라서 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)그런 다음 요청을 전달하는 일치 함수를 호출하여 일치가 수행됩니다.
이렇게 하면 NavKey가 포함된 null을 허용하는 MatchResult가 제공됩니다. 이 예에서는 세션 키가 되며, 결과가 null이면 홈 화면으로 기본 설정할 수 있습니다.
이제 여러 딥 링크를 여러 일치자로 작성해야 합니다(딥 링크당 일치자 하나).
예를 들어 여기에는 세션 키에 대한 두 가지 URI 패턴이 있습니다. 하나는 기존 뉴스레터 패턴이고 다른 하나는 일반 웹 URI입니다.
또한 ADS 홈 화면으로 연결되는 딥 링크가 있으므로 이를 위한 세 번째 일치자를 추가합니다.
그리고 모든 일치자는 반복하는 동일한 목록에 추가됩니다.
이제 일치 항목은 목록의 모든 일치 항목을 반복하는 루프가 되며, 각 일치 항목에서 일치 항목을 호출하여 요청과 일치하는 모든 일치 항목을 찾습니다.
이렇게 하려면 mapNotNull를 사용하여 일치하지 않는 항목을 필터링하고 유효한 일치 항목만 반환하면 됩니다.
val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}대부분의 경우 목록에 null이 아닌 일치 항목이 하나만 있지만 유효한 일치 항목이 두 개 이상 있을 수 있습니다.
예를 들어 동일한 세션 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 직렬화를 사용하여 딥 링크 요청에서 필드를 추출하고, 앞서 보여드린 세션 클래스와 같은 유형 안전 클래스로 만듭니다.
여기에 작성된 것처럼 이는 매우 간단한 변환입니다.
URI는 문자열입니다.
필드는 문자열입니다.
정말 간단하네요.
하지만 문자열 속성만으로는 충분하지 않다는 것을 알고 있었습니다.
이 부분에서 Kotlin 직렬화는 이미 작업에 사용할 수 있는 탄탄한 기반을 제공합니다.
불리언, 정수, 부동 소수점 수, Enum과 같은 모든 기본 유형은 즉시 작동하며, 딥 링크 값을 특정 유형으로 파싱할 수 없는 경우 자동으로 일치에 실패하고 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여기에서 볼 수 있듯이, Google은 웹 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을 변경할 수 없더라도 해당 URL에서 일부 정보를 추출해야 하는 경우에도 해결 방법이 있습니다.
첫 번째 기법은 DeepLinkSerializer 클래스를 확장하여 인수가 문자열에서 유형 안전 속성으로 역직렬화되거나 변환되는 방식을 정확하게 제어하는 것입니다.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}DeepLinkSerializer는 Kotlin 직렬화를 둘러싼 도우미 래퍼로, 직렬 변환기 클래스 구현을 간소화합니다. 이 경우 역직렬 변환기 입력은 항상 웹 URL의 문자열이기 때문입니다.
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>이제 DeepLinkSerializer를 구현하려면 세 가지 메서드를 작성합니다.
먼저 연속 프로그램 이름을 선택합니다.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}네임스페이스를 지정할 필요는 없지만 Kotlin 직렬화에서 직렬 변환기를 사용해야 하는 시점을 알 수 있을 만큼 고유해야 합니다.
다음으로, 웹 URL에서 해당 문자열을 전달받아 유형 안전 객체를 구성하는 deserialize를 구현해야 합니다.
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하지만 base64 문자열이나 웹 URL에 연결된 JSON에서 일부 데이터만 가져오려면 어떻게 해야 할까요?
이 경우 Kotlin 직렬화에 도달하기 전에 일부 웹 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 직렬화를 전혀 건드릴 필요가 없습니다.
이안, 감사합니다.
이러한 기법을 자주 사용할 필요는 없겠지만, Google은 일치 동작을 제어하는 방법에 대해 많은 고민을 했습니다.
좋습니다.
이제 딥 링크 일치자를 사용하여 NavKey를 가져오는 방법을 알았지만 탐색 키만 있는 백 스택은 최상의 사용자 환경을 제공하지 못할 수 있습니다.
따라서 적절한 뒤로 동작을 보장하려면 합성 백 스택이 필요합니다.
합성 부분은 백 스택에 사용자가 수동으로 이동하지 않은 키가 포함되어 있지만 수동으로 이동한 경우 사용자가 이동했을 경로라는 사실에서 비롯됩니다.
예를 들어 곧 시작될 세션에 관한 알림을 클릭하면 사용자가 해당 세션으로 바로 이동하지만, 시스템 뒤로를 트리거하면 런처로 종료하기 전에 HomeScreen으로 돌아가도록 할 수 있습니다.
이 HomeScreen은 합성 백 스택의 일부가 됩니다.
딥 링크 API의 경우 두 가지 상충되는 요구사항의 균형을 맞추고자 했습니다.
한편, 딥 링크나 일치자가 다르면 합성 백 스택을 빌드하는 로직도 달라야 한다는 것을 알고 있습니다.
예를 들어 모든 딥 링크 앞에 HomeScreen을 추가하면 설정 화면 내의 하위 화면으로 딥 링크를 연결할 때까지 작동할 수 있습니다.
이제 특정 딥 링크의 합성 백 스택에 추가 레이어가 필요합니다.
당연히 이로 인해 백 스택 빌더가 각 딥 링크 일치자에 연결됩니다.
하지만 올바른 화면이 표시되기까지 걸리는 시간이 길어질수록 비용이 발생한다는 점도 알고 있습니다.
따라서 모든 일치 결과에 대해 합성 백 스택을 계산하고 싶지 않았습니다.
가장 일치하는 항목만 원했습니다.
각 매처에 맞춤 로직을 사용하고 싶지만 각 일치 항목에 대해 합성 백 스택을 적극적으로 만들고 싶지 않다면 어떻게 해야 할까요?
몇 번의 시행착오 끝에 두 요구사항의 균형을 맞추는 접근 방식을 찾았습니다.
어떤 종류의 매처가 있든 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의 서브클래스로, withBackStack 람다에 의해 생성된 전체 백 스택인 backStack라는 새 속성을 추가합니다.
이러한 계층화 접근 방식을 통해 매처 내부의 로직을 withBackStack 람다 내부에서 사용하려는 커스텀 로직과 분리할 수 있었습니다.
라이트닝 토크이기 때문에 아쉽게도 시간이 거의 다 되었지만, Navigation 3 레시피에서 찾을 수 있는 Navigation 3의 다른 기능 몇 가지를 살펴보겠습니다.
좋습니다.
따라서 일치자에 필터를 추가하여 일치 로직을 단축하고 작업, MIME 유형 또는 자체 맞춤 필터를 기반으로 일치자를 빠르게 제외할 수 있습니다.
이는 URI 파싱이나 맞춤 직렬화, Kotlin 직렬화를 거치기 전에 발생합니다.
또한 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 서브클래스는 이를 로직의 일부로 사용할 수 있습니다.
이를 통해 AB 테스트 프레임워크나 외부 신호를 혼합할 수 있습니다.
또한 추가 항목을 위한 전체 유형 안전 API도 있습니다.
val extras = requestExtras {
put(SectionExtrasKey, "A")
}이러한 로직은 대부분 Nav3의 공통 KMP 코드에 있으므로 모든 매처와 백 스택 로직을 플랫폼 간에 공유할 수 있습니다.
Navigation 3에서 딥 링크를 지원하는 방식을 더 여유롭게 살펴보고 싶다면 Nav3 레시피를 확인하세요.
예, 딥 링크 API를 DI 레시피와 Hero 앱인 NowInAndroid에도 통합했습니다.
라이트닝 토크에 참석해 주셔서 감사합니다. 즐겁게 탐색해 보세요.
감사합니다.
val matcher = UriDeepLinkMatcher(
...,
...
)val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(...),
...
)val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/home"),
...
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
…
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
serializer = serializer<Session>()
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)// 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)val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}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
)// 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>>// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
val id: Int,
val referrer: String? = null,
) : NavKey// "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,
) : NavKeyobject BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}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)
}
...
}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)
}
}object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}
@Serializable data class Session(
val id: String,
@Serializable(BitmapSerializer::class)
val placeholderImage: Bitmap? = null
) : NavKeyclass 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>? {
...
}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)
}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)@Serializable object ImagesKey: NavKey
// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))val extras = requestExtras {
put(SectionExtrasKey, "A")
}