ursus
04/18/2026, 1:29 PMNav3 is there a way to let the callsite decide to transition animation (or lack there of)?
Other than manually attaching some enum to the key and changing the respective Entry accordingly, which will be super laborous (if there is any other magical way)
Maybe some decorator or something central like that?
The actual use case is deeplinks, onNewIntent the standard horizontal slide animation looks too distracting - none or fade would be preferred UX hereKamilH
04/18/2026, 6:54 PMursus
04/18/2026, 6:57 PMtransitionSpec
fun <T : Any> pushSlideAndFadeTransitionSpec(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform = {
val navKey = targetState.key
if (navKey is DeeplinkTargetNavKey && navKey.isDeeplink) {
fadeIn(animationSpec = tween(700)) togetherWith
fadeOut(animationSpec = tween(700))
} else {
standardTransitionSpec()
}
}
something like this, but turns out the key here is stringandrew
04/19/2026, 10:56 PMursus
04/20/2026, 7:29 PMandrew
04/20/2026, 10:17 PMandrew
04/20/2026, 10:18 PMentry<MainStack.Provisioning>(
metadata = buildMap {
var useMask = true
putAll(NavDisplay.transitionSpec {
val isFromSelect = (initialState.key as String)
.contains(MainStack.ProvisioningSelect::class.simpleName.toString())
if (isFromSelect) {
useMask = false
slideInHorizontally { it } + fadeIn() togetherWith
slideOutHorizontally { -it } + fadeOut()
} else null
} + NavDisplay.popTransitionSpec {
applyProvisioningSlidePopTransition()
} + NavDisplay.predictivePopTransitionSpec {
applyProvisioningSlidePopTransition()
})
// Use edge clip mask only when initiated from device info screen.
if (!useMask) putAll(NavDisplay.ignoreMask())
}
) { entry ->
val type = entry.type
val model = entry.model
val locationId = entry.locationId
val deviceId = entry.deviceId
ProvisioningRootScreen(type, model, locationId, deviceId)
}
With the apply transition function:
private fun AnimatedContentTransitionScope<Scene<*>>.applyProvisioningSlidePopTransition(): ContentTransform? {
val isTowardsSelect = (targetState.key as String)
.contains(MainStack.ProvisioningSelect::class.simpleName.toString())
return if (isTowardsSelect) {
slideInHorizontally { -it } + fadeIn() togetherWith
slideOutHorizontally { it } + fadeOut()
} else null
}andrew
04/20/2026, 10:18 PMandrew
04/20/2026, 10:19 PMursus
04/20/2026, 10:20 PMval isFromSelect = (initialState.key as String).contains(MainStack.ProvisioningSelect::class.simpleName.toString())
isn't this exactly the same as I have in the original post? i.e. dealing with stringified key?andrew
04/20/2026, 10:20 PMandrew
04/20/2026, 10:21 PMandrew
04/20/2026, 10:21 PMursus
04/20/2026, 10:21 PMandrew
04/20/2026, 10:21 PMursus
04/20/2026, 10:22 PMandrew
04/20/2026, 10:22 PMandrew
04/20/2026, 10:24 PMandrew
04/20/2026, 10:26 PMursus
04/20/2026, 10:50 PMursus
04/20/2026, 10:50 PMandrew
04/20/2026, 11:06 PMursus
04/20/2026, 11:21 PMVersion 1.1.0-alpha03
January 28, 2026
androidx.navigation3:navigation3-*:1.1.0-alpha03 is released. Version 1.1.0-alpha03 contains these commits.
New Features
You can now dynamically add metadata with consideration for the entry key via the EntryProvider DSL. (I942fb, b/474416976)
I wasn't even aware that at entry declaration you don know the key at metadata time
and the comment/changelog says there should be a way now?ursus
04/20/2026, 11:23 PMpublic inline fun <reified K : T> entry(
@Suppress("KotlinDefaultParameterOrder")
noinline clazzContentKey: (key: @JvmSuppressWildcards K) -> Any = { defaultContentKey(it) },
noinline metadata: (K) -> Map<String, Any>,
noinline content: @Composable (K) -> Unit,
) {
addEntryProvider(K::class, clazzContentKey, metadata, content)
}
this overloadursus
04/20/2026, 11:26 PMentry<Details>(
metadata = { key: Details ->
metadata {
put(NavDisplay.TransitionKey) {
if (key.fromDeepLink) {
fadeIn() togetherWith fadeOut()
} else {
slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
}
}
put(NavDisplay.PopTransitionKey) {
if (key.fromDeepLink) {
fadeIn() togetherWith fadeOut()
} else {
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
}
}
}
}
) { key ->
DetailsScreen(id = key.id)
}ursus
04/20/2026, 11:30 PMval entries = entryProvider<NavKey> {
entry<Home>(metadata = { key -> defaultMetadata(key) }) { HomeScreen() }
entry<Details>(metadata = { key -> defaultMetadata(key) }) { DetailsScreen(id = key.id) }
entry<DialogLike>(metadata = { key -> defaultMetadata(key) }) { DialogScreen() }
}
doesn't scale and there surely needs to be a global wayKamilH
04/22/2026, 7:01 AMNavKey with your own type and then using custom entry function that decides which transition it should apply:
data class NavRecord(
val fromDeepLink: Boolean,
val navKey: NavKey,
)
class Navigator {
private val backstack: MutableList<NavRecord> = mutableListOf()
fun push(navKey: NavKey) {}
fun deepLink(navKey: NavKey) {
backstack.add(
NavRecord(
fromDeepLink = true,
navKey = navKey,
)
)
}
}KamilH
04/22/2026, 7:09 AMinline fun EntryProviderScope<NavRecord>.entry(noinline content: @Composable (NavKey) -> Unit) {
entry<NavRecord>(
metadata = { key ->
metadata {
put(NavDisplay.TransitionKey) {
if (key.fromDeepLink) {
fadeIn() togetherWith fadeOut()
} else {
slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
}
}
put(NavDisplay.PopTransitionKey) {
if (key.fromDeepLink) {
fadeIn() togetherWith fadeOut()
} else {
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
}
}
}
},
content = {
content(it.navKey)
}
)
}ursus
04/22/2026, 9:10 AMentry extension usage, i.e. that I cannot use the default one and skip the machinery?andrew
04/23/2026, 2:27 AM