Software Engineer
Managing deep links across modular Android apps can easily lead to broken routes and runtime surprises.This session unpacks Jetpack Navigation3’s new type-safe deep linking architecture for Compose to help you build a robust and scalable routing system. We'll explore how to implement out-of-the-box deep link matching APIs, write custom serializers for complex type-safe arguments, and construct dynamic hierarchical back stacks that keep user navigation predictable.
DeepLinkRequest class encapsulates the input, including an Android Intent constructor.UriDeepLinkMatcher uses Kotlin Serialization to match URI patterns and deserialize arguments into type-safe NavKey properties. Primitive types work automatically, failing matches if values cannot be parsed.DeepLinkSerializer or override matchArguments to handle complex deep link strings. Subclassing UriDeepLinkMatcher lets you preprocess raw URI placeholders before Kotlin Serialization runs.withBackStack on matchers creates synthetic back stacks lazily for the best match. This ensures proper system back behavior without computing unnecessary back stacks for every other valid match.Transcript
Hi, I'm Ian Lake, the engineering manager for the Jetpack Architecture team.
And I'm Clara Fok, the tech lead for Nav 3.
And today, we're here to talk to you about a feature that's near and dear to my heart, Nav-- deep links.
In fact, I like them so much I did the implementation for both Nav2 and Nav3 as part of Nav3's 1.2 release.
I think you actually did it three times, cuz we also did an implementation in the Nav 3 recipes, as well.
Yes, that is true.
We went through a lot of iteration on the API to make sure it feels at home with Nav3's Compose-first approach.
And a lot of that started with our emphasis on recipes.
Yeah, one of our main goals for Navigation 3 was to get out of your way.
When you have a particularly ambitious feature to build, it shouldn't require a feature request from us to actually make it possible.
So, when we get that question of, "All right, how do I do this?"
Or, "How do I even approach this problem?"
Instead of building a custom API right from the get-go, we instead build a recipe, a guide that explains how you could add that feature to your app without necessarily needing any changes in Navigation 3 at all.
It was that style of showing, not telling, that led us to create the Nav3 recipes repository on GitHub.
It now has over 35 recipes for common use cases like multiple back stacks and bottom sheets that gives you all the code you need to implement it in your app.
That meant that even for the 1.0 release of Nav3, back in November 2025, we already had a recipe for implementing deep links because we knew that was important to many of you.
But when we realize that a recipe requires having a bunch of helper methods that you need to copy and paste into your app, that's when we consider a process called graduation, where we take those helpers and say, "All right, what would that actually look like if they were part of navigation three itself?"
This ensures that developers that have already copy-pasted that code, it's fine.
You can keep that code.
Any custom solutions you have are, if they're working, that's great.
You don't have any forced migration.
But if it's a brand new problem for you or a new requirement, you get an easy, vetted solution that allows you to kind of use that directly as part of navigation.
But the best part, for me personally, is that after we add that helper, we go back and update our recipes and get that sweet dopamine hit of deleting code.
That way, anyone who's looking at the recipes for kind of the recommended approach for that problem has the most up-to-date solution.
Of course, hindsight is 20/20, and that graduation process gives us an opportunity to really look at those helpers we built in the recipes and say, "Are those actually the best APIs for it?"
And in this case, it gives us an opportunity to kind of improve those things as we move it into the library.
And that was definitely the case for deep links.
Thank you, Ian.
So, conceptually, both the recipes and our final APIs are very similar, because in Nav 3, you own the Back Stack.
That means that focus of our deep link APIs is to figure out what that initial Back Stack should be, specifically what screen do your users see when they click on a deep link to enter your app.
But what actually happens when the user clicks on that web URI?
Now, whether you use Nav3 or not, the Android framework requires that you declare Intent Filters for each deep link that your app supports.
Now, where it gets interesting is what happens once that deep link triggers your Activity to start.
Here, the input is the Intent that your app is started with, that Activity that started.
But the deep link can take many forms.
For example, you may want to construct it manually for a test, or you may want to take advantage of your Kotlin Multiplatform support.
So, we've encapsulated the deep link input into a KMP-compatible DeepLinkRequest class.
And on Android, we have a DeepLinkRequest constructor that takes in an Android Intent.
Next, our goal is to figure out, given a request, how do we know what NavKey needs to be put on your Back Stack?
This is where our DeepLinkMatcher class comes in.
It takes in a request and outputs a MatchResult, which contains the NavKey associated with the match.
Specifically for web URIs, we have a UriDeepLinkMatcher that encapsulates all the logic needed to transform a given request into a MatchResult.
val matcher = UriDeepLinkMatcher(
...,
...
)Now, the first parameter that a UriDeepLinkMatcher takes is a URI pattern.
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(...),
...
)So, let's say you were building an ADS app, and you want to add a deep link to the home screen.
Here, the pattern is an exact URI, and we can leave out the HTTP schema to automatically match both HTTP and HTTPS.
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/home"),
...
)But if we wanted to deep link to a particular ADS session, now that deep link contains information that we need to extract, namely the session ID.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
…
)So in this case, our pattern uses curly braces to act as placeholders for information that we need to extract from the deep link.
And it turns out there's already a handy library to convert any kind of input into class instances, which is Kotlin Serialization.
So our UriDeepLinkMatcher leverages a Kotlin decoder to deserialize an input URI and output a NavKey instance.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
serializer = serializer<Session>()
)Now, that decoder needs to know what class to work with, so you'll also need to pass in a serializer for your NavKey into the matcher.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)And to help the decoder properly match match placeholders to the key's properties, the placeholder's name must match the property names.
So in this example, the property name is ID matching the session's ID parameter.
Okay, now that we have our UriDeepLinkMatcher, we need to construct a DeepLinkRequest with the incoming deep link.
// 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)Then matching is done by calling the match function, passing in our request.
This will give you a nullable MatchResult containing the NavKey, which in our example would be the session key, or if the result is null, we can just default to the home screen.
Now, multiple deep links should be written as multiple matchers: one matcher for each deep link.
So for example, here we have two URI patterns for our session key: one for a legacy newsletter pattern and one for a regular web URI.
And we also have a deep link to the ADS home screen, so we add a third matcher for that.
And all the matchers would be added to the same list that we iterate through.
So now our match becomes a loop over all the matchers in our list, where we call match on each one to find all the matchers that matches the request.
To do that, we could use mapNotNull to filter out the non-matches and get back a list of only the valid matches.
val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}Now in most cases, you'll probably only have one non-null match in your list, but there are cases where there can be more than one valid match.
For example, let's say we introduce a special keynote screen reusing the same session URI pattern, but now instead of the ID placeholder, it has a static keynote path.
So in this case, the same request can match both matchers: the session key where the ID equals keynote and the keynote NavKey.
So since MatchResult implements Comparable, we can find the best match by comparing all of the valid matches in the list using the max function, or really maxOrNull to handle the case where there weren't any deep links matching the 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
)Now, a static list of matchers might make sense for a lightning talk, but just like how your screens can be spread across multiple modules, we expect those same modules to contribute to a global list of matchers via your DI system.
// 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>>And using this approach, our Hilt, Metro, and Koin recipes in our recipes repo have been updated to include deep links.
Now, Clara mentioned that our UriDeepLinkMatcher uses Kotlin Serialization to extract fields out of the deep link request and make it into our type-safe class, like our session class we showed.
As written here, this is a pretty simple transformation.
The URI is a string.
The field's a string.
Wow, that was easy.
But we know that we needed to go beyond just string properties.
This is where Kotlin Serialization already gives us a solid foundation to work with.
All the primitive types, like Booleans, integers, floats, even Enums work out of the box, automatically failing the match and returning null if the deep link value can't be parsed as that specific type.
For example, if our session's ID parameter were actually an integer instead of a string, passing in a string that couldn't be parsed in integer would fail the match.
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKeySimilarly, if you wanted to have optional arguments, say for analytics data attached to your web URL, those could be properties on your key that have default values.
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
val id: Int,
val referrer: String? = null,
) : NavKeyYou'll note here we've tried really hard to decouple the structure of your web URL, that it might be a different team that owns, from the type-safe classes or classes that represent the deep link match in your app.
Because I know some of you are handling some absolutely terrible deep links.
// "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,
) : NavKeyYou know, maybe you have a base64 encoded image attached to your deep link.
You know, maybe a giant JSON blob that they decided, "Oh, let's just put that into every URL of your app."
Now, there's a path forward, even when you can't change the web URL you're given, but you still need to extract some information out of that.
So, the first technique is to extend our DeepLinkSerializer class to control exactly how that argument is deserialized or transformed from a String into your type-safe property.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}The DeepLinkSerializer is a helper wrapper around Kotlin Serialization that simplifies implementing their serializer class, because in this case, the deserializer input is just always a String from your web URL.
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>Now to implement a DeepLinkSerializer, you write three methods.
First, you choose a serial name.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}Doesn't need to be namespaced, but it needs to be unique enough that Kotlin Serialization knows when to use your serializer.
Next, you need to implement deserialize, which is passed that String from your web URL and is responsible for constructing your type-safe object.
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)
}
...
}Last, you'll implement serialize, which is the opposite approach, and this is used if your same key class is used in like rememberNavBackStack cases.
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)
}
}Now you'll apply your custom serializer to the property that has that class that's a little special, and web UriDeepLinkMatcher will take care of the rest.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}
@Serializable data class Session(
val id: String,
@Serializable(BitmapSerializer::class)
val placeholderImage: Bitmap? = null
) : NavKeyBut what if you only want a little bit of the data from that base 64 String, or that JSON that was attached to your web URL?
In that case, it might make sense to pre-process some of your web URL before it gets to Kotlin Serialization at all.
To handle those cases, you can extend UriDeepLinkMatcher and override its matchArguments method.
Now, maybe for our JSON blob, all we really want is just that session ID, like we actually don't care about any of the rest of that.
So we create our sub-class and override 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>? {
...
}The inputs are those raw strings, all the place holders that were added from your URI path.
And that allows us to pre-process and edit those values before we call into 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)
}Now, we can have simplified values here, and we don't need to mess with Kotlin Serialization at all.
Thanks, Ian.
Hopefully you won't have to reach out to those techniques often, but we did put a lot of thought into how you can control the matching behavior.
Okay.
Now that we know how to get a NavKey with the deep link matchers, but a back stack with only the navigation key may not provide the best user experience.
So, to ensure the proper back behavior, you'd want a synthetic back stack.
The synthetic part comes from the fact that the back stack contains keys that the user never manually navigated to, but it is the path that they would have taken if they did manually navigate there.
So, for example, clicking on a notification for a session that's about to start, you'd take the user directly to that session, but when they trigger system back, you'd probably want them to go back to your HomeScreen before exiting to the launcher.
That HomeScreen becomes part of the Synthetic Back Stack.
Now for deep link APIs, we wanted to compete we wanted to balance two competing requirements.
On the one side, we know that different deep links, or different matchers, would want different logic for building their synthetic back stack.
So for example, adding a HomeScreen before every deep link might work until you want to deep link to a sub-screen inside a setting screen.
Now, you need an extra layer in your synthetic back stack for that particular deep link.
Naturally, this ties the back stack builder to each deep link matcher.
But on the other hand, we also know that every millisecond leading up to the correct screen being displayed costs you all real money.
So we didn't want to compute a synthetic back stack for every match result.
We wanted just for the best match.
So, if we want custom logic for each matcher, but we don't want to create a synthetic back stack eagerly for each match, what do we do?
So after a few false starts, we found an approach that balances both requirements.
No matter what kind of matcher you have, you can augment that specific matcher by calling a withBackStack to lazily construct the back stack with the given 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)So, if we have multiple matchers, each one would have its own withBackStack call to build the appropriate back stack for that specific NavKey.
You'll get the cleanest code if every matcher you call uses the withBackStack, which will give you a list of back stack matchers.
And when you do find the best match, the match result you get back is a BackStackMatchResult.
This is a sub-class of MatchResult that adds a new property called backStack, which is the whole back stack generated by your withBackStack lambda.
This layering approach allowed us to decouple the logic inside the matchers from whatever custom logic you want to use inside your withBackStack lambda.
Now, this is a lightning talk, so unfortunately, we're just about out of time, but maybe we can go through a few of the other features that we have in navigation three that you'll find in our nav three recipes.
Okay.
So, you can add filters to any matcher to short-circuit the matching logic and quickly exclude the matcher based on action, MIME type, or your own custom filters.
And this happens before going through any URI parsing or custom serialization, Kotlin Serialization.
We also offer a StaticKeyDeepLinkMatcher, specifically for cases where you don't need to do any parsing to get your NavKey, but you still want to apply filters.
@Serializable object ImagesKey: NavKey
// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))This is perfect for handling action- or MIME-type-only deep links, such as a photo-editing app that can handle opening JPEG images.
And speaking of action and MIME type, you can attach any custom RequestExtras to your DeepLinkRequest, which your custom filters and DeepLinkRequest subclasses can use as part of their logic.
This lets you mix in AB testing frameworks or external signals.
And we also have a whole type-safe APIs for the extras.
val extras = requestExtras {
put(SectionExtrasKey, "A")
}Oh, and just about all this logic is in Nav3's common KMP code, so all your matchers and back stack logic can be shared across platforms.
So, if you want to take a more leisurely look through how Navigation 3 supports deep links, check out our Nav3 recipes.
Yes, and we've also integrated our deep link APIs into our DI recipes, as well as our hero app, NowInAndroid.
Thanks for coming to our lightning talk, and happy navigating.
Thank you.
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")
}