Coverage Summary for Class: BackNavigationNavEntryDecoratorKt (com.georgeci.moneysurfer.navigation.util)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| BackNavigationNavEntryDecoratorKt |
100%
(1/1)
|
100%
(4/4)
|
|
100%
(10/10)
|
100%
(106/106)
|
package com.georgeci.moneysurfer.navigation.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavEntryDecorator
import com.georgeci.moneysurfer.uikit.navigation.LocalSurferCanNavigateBack
/**
* Publishes [LocalSurferCanNavigateBack] to every rendered entry, so a screen's toolbar can drop a
* back arrow that leads nowhere.
*
* A decorator rather than a wrapper around the entry provider: the flag has to follow the back
* stack, and only the decoration runs again when the stack changes underneath an entry that stays
* composed — selecting the section a screen is already in resets the stack to just that screen, and
* its arrow has to go with the entries that were below it.
*
* Takes the [backStack] rather than the keys [backNavigationContentKeys] derives from it, so the
* navigation host is left with one call and nothing to hold: which entries can go back is this
* file's business, and the host has no test harness to cover it in.
*
* The keys are derived on every composition rather than through a `derivedStateOf`. Memoizing them
* would have to key on [entryProvider], which the host rebuilds each composition, so the cache
* would miss every time anyway — and a back stack is a handful of entries. Only the *decorator* is
* memoized, on the keys themselves, so an unchanged stack does not hand `NavDisplay` a new one.
*/
@Composable
fun <T : Any> rememberBackNavigationNavEntryDecorator(
backStack: List<T>,
entryProvider: (T) -> NavEntry<T>,
): NavEntryDecorator<T> {
val poppableContentKeys = backNavigationContentKeys(backStack, entryProvider)
return remember(poppableContentKeys) {
NavEntryDecorator { entry ->
CompositionLocalProvider(
LocalSurferCanNavigateBack provides (entry.contentKey in poppableContentKeys),
) {
entry.Content()
}
}
}
}
/**
* The `contentKey`s of the [backStack] entries that have another entry below them — the ones a back
* affordance can actually pop.
*
* Keys are resolved through the same [entryProvider] the host renders with, rather than derived
* from the route: what a route's content key is, is that provider's decision.
*
* A route that appears on the stack twice, once at the bottom, contributes one key for both
* occurrences and so keeps its affordance at the bottom too. Nothing in the app pushes a route on
* top of itself — the sections are reached with `AppNavigator.resetTo` — and the alternative,
* dropping the affordance on the *upper* copy, strands the user on it.
*/
internal fun <T : Any> backNavigationContentKeys(
backStack: List<T>,
entryProvider: (T) -> NavEntry<T>,
): Set<Any> = backStack.drop(1).mapTo(mutableSetOf()) { entryProvider(it).contentKey }