In `Nav3` is there a way to let the callsite decid...
# compose
u
In
Nav3
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 here
k
Since alpha03 there is a new way to dynamically add metadata, so you can dynamically decide if you want to perform transition or not https://developer.android.com/jetpack/androidx/releases/navigation3#1.1.0-alpha03
u
well that's the laborous manual callsite way, no? I was hoping having bit of code in the `NavDisplay`global
transitionSpec
Copy code
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 string
a
I have an example of this, I think and I can send it, I just need to open my work laptop... If I don't do it tonight it'll be Monday
u
@andrew knock knock
a
I'll send it 🙂
Copy code
entry<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:
Copy code
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
}
Here's a similar example
Not using deeplink intents here, but you can dynamically adjust metadata and its okay
u
I mean
Copy code
val 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?
a
Let me get a screen recording for you so you can see it in action
Yeah, I probably should be using is comparisons
I think there was a reason that I wasn't
u
I trust you it works, its basically what I have, but you have it at entry declaration site, which I dont really want, as I'd have to put it to 100 places
a
You could use extension functions to help with that I'm sure
u
doesn't scale
a
Or make it a function, forgot if it's scoped
Yeah, I needed it to be a string in this case, doesn't work otherwise (which is annoying)
Here it is in action (fade/scale vs slide/slide)
u
yea the string gives me a pause
but thanks!
a
Yeah, not fan of the string either, but if theres a better way to get the key type, I'm all ears
u
I'd think its in the first post in here
Copy code
Version 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?
Copy code
public 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 overload
Copy code
entry<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)
}
anyways my point was that this
Copy code
val 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 way
k
You can achieve this by simply wrapping the
NavKey
with your own type and then using custom
entry
function that decides which transition it should apply:
Copy code
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,
			)
		)
	}
}
and then something like:
Copy code
inline 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)
        }
    )
}
u
Nice, that was my plan B since.. do you see a way to enforce this
entry
extension usage, i.e. that I cannot use the default one and skip the machinery?
a
Hmmm, I am going to try incorporating some of this into my code since it introduces type safety again.