Using Navigation3, I would like to achieve generic...
# compose
o
Using Navigation3, I would like to achieve generic transition spec based on
NavKey
subclass specific properties. Say I have
MyAppNavKey
which extends
NavKey
and this interface defines a property to be implemented by final routes/NavKey. The idea would be to define the transition spec at
NavDisplay
level, based on
targetState
or
initialState
, and
MyAppNavKey.someProperty
, I could take the decision of which transition to apply. The issue I have is that I can't access
NavEntry.key
which is private, but this is the object I'd need to take the decision.
ContentKey
is a string I can't rely on. Ideally, I don't want to impact any
entry<Foo>
call sites of the
entryProvider {}
, everything should be statically declared in each route.
data object MyFinalRoute(override val someProperty: Boolean) : MyAppNavKey
Here is the code I'd like to write
Copy code
transitionSpec = {
    val targetKey = targetState.entries.lastOrNull().key as? MyAppNavKey
    val isTargetModal = targetKey?.shouldDisplayAsModal ?: false

    if (isTargetModal) {
        slideInVertically { fullHeight -> fullHeight } togetherWith ExitTransition.KeepUntilTransitionsFinished
    } else {
        slideInHorizontally { fullWidth -> fullWidth } togetherWith slideOutHorizontally { fullWidth -> -fullWidth }
    }
},
s
You can set the transition for each route in the metadata when you define the Nav entry, using NavDisplay.transitionSpec. Sample NavDisplay: https://github.com/sebvil/BgComp/blob/main/navigation%2Fsrc%2FcommonMain%2Fkotlin%2Fcom%2Fsebastianvm%2Fbgcomp%2Fnavigation%2Fui%2FNavHostUi.kt
o
Yes, I started like this, but it requires to update each route call sites (
entry<R>
) which painful. Also, some routes are conditionally defined, the custom property of the route driving the transition might depend on some condition (thus, the property being defined at route level). So, it's too static too.
i
Sounds like you already know where this should be set: at the entry level, where you do have access to the key and can use custom properties in your key to change your transition. It sounds like what you actually want to write is your own wrapper around
entry
ala
myAppEntry
that you enforce your developers to use which is what sets the default metadata. That way, you still define the logic in exactly one place
o
I tried that but I'm not sure to fully understand how to do that. When I look at the
entry<R>
implementation 1. I can't duplicate it and reuse original impl 2. I don't see how I can get the key object (only the key class) at this point 🤔
I'm using the
entryProvider {}
DSL, maybe I can do it without it?
I tried by removing the DSL and now I can get the key object to inject metadata in it. This way, I can deal with it in the generic transition specs.
Copy code
fun createMetadata(key: MyAppNavKey): Map<String, Boolean> = mapOf(
    "modal" to key.shouldDisplayAsModal
)

fun <T : MyAppNavKey> NavEntry<T>?.isModal() = this?.metadata?.get("modal") == true
fun <T : MyAppNavKey> Scene<T>.isModal() = entries.lastOrNull().isModal()
Copy code
NavDisplay(
        ...
        transitionSpec = {
            val isTargetModal = targetState.isModal()
            if (isTargetModal) ... else ...
        },
        popTransitionSpec = {
            val isInitialModal = initialState.isModal()
            if (isInitialModal) ... else ...
        },
        ...
        entryProvider = { key ->
            val metadata = createMetadata(key)
            when (key) {
                MyRoute -> NavEntry(key, metadata = metadata) {
                    ...
                }
                ...
            }
        }
    )
}
I imagine, I could create my own DSL to simplify this and avoid the fact that each route must bind the proper metadata. Is that what you suggested? Do you think there is a way to keep the original DSL impl and achieve the metadata computation properly JUST with a custom
myAppEntry
function?
Copy code
class EntryBuilderScope<K : NavKey> {
    val map = mutableMapOf<KClass<*>, (K) -> NavEntry<K>>()

    inline fun <reified T : K> entry(noinline block: @Composable (T) -> Unit) {
        map[T::class] = { key ->
            val isModal = (key as? LotoNavKey)?.shouldDisplayAsModal ?: false
            val metadata = mapOf("modal" to isModal)
            NavEntry(key, metadata = metadata, content = { block(key as T) })
        }
    }

    fun build(): (K) -> NavEntry<K> = { key ->
        map[key::class]?.invoke(key)
            ?: error("No entry registered for ${key::class}")
    }
}

fun <K : NavKey> modalAwareEntryProvider(routes: EntryBuilderScope<K>.() -> Unit) = EntryBuilderScope<K>().apply(routes).build()

private fun <K : NavKey> NavEntry<K>?.isModal() = this?.metadata?.get("modal") == true
private fun <K : NavKey> Scene<K>.isModal() = entries.lastOrNull().isModal()

fun <K : NavKey> modalAwareTransition(): AnimatedContentTransitionScope<Scene<K>>.() -> ContentTransform = {
    val isTargetModal = targetState.isModal()

    if (isTargetModal) {
        slideInVertically { it } togetherWith ExitTransition.KeepUntilTransitionsFinished
    } else {
        slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
    }
}

fun modalAwarePopTransition(): AnimatedContentTransitionScope<Scene<LotoNavKey>>.() -> ContentTransform = {
    val isInitialModal = initialState.isModal()

    if (isInitialModal) {
        EnterTransition.None togetherWith slideOutVertically { it }
    } else {
        slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
    }
}
Here a working custom impl with DSL Not 100% satisfied to not rely on the built-in DSL "just" because
NavEntry<R>.key
is private 😅
i
I wonder if this is more of a case of an XY Problem where your solution to one problem (adding
shouldDisplayAsModal
to every key in your whole app) is what caused this additional problem (how can I get the
shouldDisplayAsModal
from my key out). Maybe it would be better to take a step back and talk about what brought you to adding
shouldDisplayAsModal
in the first place, because generally that is not what should be in your key in the first place - the key should be the identity of your screen, not a place to stash unrelated flags
o
Yes, maybe it can be the case. My initial need is to convey the information that I want to go in a specific screen conditionally being displayed as modal or not. It felt natural to proceed this way, I'm not sure I can see an alternative. Would you have idea?
(in this particular case, it's for settings screen being displayed contextually from another screen, being displayed as modal, but in some other circumstances, the same screen should be displayed with push transition)