Annotation reference¶
The annotations live in the codegen-annotations artifact under the package io.github.thomaskioko.codegen.annotations. All have SOURCE retention.
Seven annotations:
@NavDestinationtargets presenter classes in the shared Kotlin Multiplatform layer. The processor generates a Metro@GraphExtensionplus a navigation binding for each annotated presenter.@ScreenUi,@SheetUi, and@TabUitarget@Composablefunctions in Androiduimodules. The processor generates the Metro binding that contributes the composable to the consumer'sSet<ScreenContent>orSet<SheetContent>multibinding.@TabUiis the variant for tab pager pages, where the active child is aTabChildrather than aScreenDestination.@ChildPresentertargets a presenter class owned by another presenter rather than navigated to through a route, such as a tab pager's pages. The processor generates a<Presenter>ChildGraphgraph extension plus a factory contributing to the parent host's scope.@AppRoottargets the application's@AssistedInjectroot presenter implementation. The processor generates the activity-scope binding container that wires the nested@AssistedFactoryto the presenter's bound interface.@AppRootUitargets the host@Composablethat wraps every other screen. The processor generates anAppRootProviderinterface plus a Composable extension so the activity invokes the host with one call (graph.AppRootContent()) instead of forwarding each dependency by hand.
This reference walks through each annotation: what it marks, what the processor emits, the validation rules, and the common pitfalls. For the vocabulary used throughout (graph extension, multibinding, binding, slot, scope), see the glossary in architecture/index.md.
@NavDestination¶
Marks a presenter class as a navigation destination. One annotation, three kinds.
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
public annotation class NavDestination(
val route: KClass<*>,
val parentScope: KClass<*>,
val kind: DestinationKind,
)
public enum class DestinationKind {
SCREEN,
OVERLAY,
TAB_ROOT,
}
When to use @NavDestination¶
Annotate a presenter class with @NavDestination whenever the consumer's navigator needs to push it onto the back stack (SCREEN), present it as a modal overlay
(OVERLAY), or render it as a top-level tab anchor (TAB_ROOT). Without the annotation the consumer has to manually write the Metro graph extension, the
NavDestination factory, and the route binding for each presenter.
Parameters¶
The route parameter points at the feature's route class.
- For
SCREENandOVERLAYit implements the consumer project'sNavRouteinterface.OVERLAYadditionally implements the consumer's overlay marker interface (for exampleOverlayRoutein Tv Maniac), which is what the consumer's navigator inspects at runtime to decide between stack and overlay routing. - For
TAB_ROOTit implements the consumer'sNavRootinterface and is typically adata object.
The parentScope parameter names the parent dependency injection scope whose factory provides a ComponentContext. Typically ActivityScope::class.
The kind parameter chooses one of three destination roles. See DestinationKind.
Minimal example¶
@Inject
@NavDestination(
route = ShowsRoute::class,
parentScope = ActivityScope::class,
kind = DestinationKind.SCREEN,
)
public class ShowsPresenter(
componentContext: ComponentContext,
) : ComponentContext by componentContext
Generated artifacts¶
For a presenter com.example.feature.presenter.FooPresenter annotated @NavDestination(route = FooRoute::class, parentScope = ActivityScope::class, kind = SCREEN):
FooScreenGraph.ktis a Metro@GraphExtension(FooRoute::class)interface scoped to the route. The route class itself is the scope marker, so no separate scope class is generated. The reasoning lives in architecture/generators.md.FooNavDestinationBinding.ktis a Metro@ContributesTo(parentScope)interface with a companion that contributes:@IntoSet NavDestination<*>: the matchingNavDestination.Screen(orOverlay, orTabRoot) instance.@IntoSet NavRouteBinding<*>forSCREENorOVERLAY, or@IntoSet NavRootBinding<*>forTAB_ROOT.@IntoSet NavRootforTAB_ROOTonly: the route singleton itself, contributed toSet<NavRoot>so consumers do not have to keep a parallel binding next to each tab.
See examples.md for concrete output for each kind.
Behavior by kind¶
| Kind | Injection | Contributes |
|---|---|---|
SCREEN |
@Inject (no runtime parameters) or @AssistedInject (parameterized) |
NavDestination.Screen plus NavRouteBinding<*> |
OVERLAY |
Same as SCREEN |
NavDestination.Overlay plus NavRouteBinding<*> |
TAB_ROOT |
@Inject only (no @AssistedInject) |
NavDestination.TabRoot plus NavRootBinding<*> plus the NavRoot singleton into Set<NavRoot> |
For SCREEN and OVERLAY the processor auto-detects whether your presenter accepts a runtime parameter from the route. A plain @Inject constructor produces a graph
that exposes the presenter directly. An @AssistedInject constructor with a nested @AssistedFactory produces a graph that exposes the factory; the generated binding
casts the route, reads the property whose type matches the presenter's single @Assisted parameter, and calls factory.create(param).
Validation¶
The processor reports a compile error if any of the following hold:
- The annotated symbol is not a class.
kindis notSCREEN,OVERLAY, orTAB_ROOT.- The presenter is parameterized but does not have exactly one
@Assistedconstructor parameter. kindisTAB_ROOTand the presenter declares a nested@AssistedFactory. Tab roots must use plain@Injectbecause the route is adata objectand carries no payload.
Common pitfalls¶
- Forgetting
@Injector@AssistedInject.@NavDestinationonly tells the processor which graph and binding to generate. The presenter still needs Metro's injection annotation so Metro creates instances of it. - Routing a tab through a
data class.TAB_ROOTrequires adata object. Adata classroute would imply a payload, but the tab multibinding is keyed by route type, not by an instance. - Mismatched route property type for parameterized presenters. The
@Assistedparameter type must match a property on the route class. The processor reads that property at navigation time and passes it through the assisted factory.
@ScreenUi¶
Marks a @Composable function as the Android renderer for a screen presenter.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
public annotation class ScreenUi(
val presenter: KClass<*>,
val parentScope: KClass<*>,
)
When to use @ScreenUi¶
Pair @ScreenUi with @NavDestination(kind = SCREEN) on the matching presenter. The presenter lives in the shared Kotlin Multiplatform layer; the composable lives in an
Android only ui module.
The annotation exists because the navigation host cannot call the composable directly. At runtime the host holds an active RootChild whose concrete presenter type is
opaque to it, so it iterates a Set<ScreenContent> multibinding to find the entry whose predicate matches the active child and renders that entry's content lambda.
@ScreenUi generates the entry that pairs the (it as? ScreenDestination<*>)?.presenter is FooPresenter predicate with a content lambda that casts the child and invokes
the annotated composable.
Parameters¶
The presenter parameter names the presenter type this screen renders. The processor uses it to build the matches predicate that tests whether the active RootChild
is a ScreenDestination<*> wrapping an instance of that type, and to cast the presenter before forwarding it to the composable.
The parentScope parameter names the dependency injection scope the generated binding is contributed to. Typically ActivityScope::class.
Minimal example¶
@Composable
@ScreenUi(presenter = ShowsPresenter::class, parentScope = ActivityScope::class)
public fun ShowsScreen(
presenter: ShowsPresenter,
modifier: Modifier = Modifier,
) {
// ... compose UI here
}
Generated artifacts¶
For a composable com.example.feature.ui.FooScreen annotated @ScreenUi(presenter = FooPresenter::class, parentScope = ActivityScope::class), the processor emits one
file into com.example.feature.ui.di:
FooScreenUiBinding.kt: a@BindingContainer @ContributesTo(ActivityScope::class) objectthat@Provides @IntoSeta singleScreenContent. Thematcheslambda tests(it as? ScreenDestination<*>)?.presenter is FooPresenter. Thecontentlambda casts the child and invokesFooScreen(presenter = ..., modifier = modifier).
The @BindingContainer object structure is deliberate. The detailed reasoning lives in
architecture/generators.md; the short version is that
an interface + companion object form would silently produce an empty multibinding inside an Android only ui module without the right Metro flag.
Composable signature requirement¶
The annotated function must match the signature @Composable fun <Name>(presenter: <PresenterType>, modifier: Modifier = Modifier). The processor does not enforce the
parameter names or the Modifier default at parse time, but the generated code calls the composable with presenter = ..., modifier = modifier. A function whose
parameters are out of order or named differently will fail to compile after generation, not during processing.
Common pitfalls¶
- Annotating a composable in a transitive
implementationdependency. The generated binding lives in the same module as the composable. If the consumer pulls in that module only as a transitive dependency, the binding never makes it onto the app's compile classpath. Add the module as a directimplementationdependency. - Returning a value from the composable.
ScreenContent.contentis(RootChild, Modifier) -> Unit. The generated wrapper expects the composable to returnUnit.
@SheetUi¶
Marks a @Composable function as the Android renderer for a modal overlay presenter. Parallel to @ScreenUi, but contributes a SheetContent instead of a ScreenContent.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
public annotation class SheetUi(
val presenter: KClass<*>,
val parentScope: KClass<*>,
)
When to use @SheetUi¶
Pair @SheetUi with @NavDestination(kind = OVERLAY) on the matching presenter. The presenter lives in the shared Kotlin Multiplatform layer; the composable lives in an
Android only ui module.
The annotation exists for the same reason as @ScreenUi: the consumer's overlay slot cannot call the composable directly. The slot host iterates a Set<SheetContent>
multibinding to find the entry whose predicate matches the active overlay child, then renders that entry's content lambda. @SheetUi generates the entry that pairs the
(it as? SheetDestination<*>)?.presenter is FooPresenter predicate with a content lambda that casts the child and invokes the annotated composable. The set is separate
from Set<ScreenContent> because the overlay slot and the navigation stack are independent registries.
Parameters¶
The parameters have the same meaning as on @ScreenUi.
Minimal example¶
@Composable
@SheetUi(presenter = EpisodeSheetPresenter::class, parentScope = ActivityScope::class)
public fun EpisodeSheet(
presenter: EpisodeSheetPresenter,
modifier: Modifier = Modifier,
) {
// ... compose your ModalBottomSheet here
}
Generated artifacts¶
For EpisodeSheet annotated @SheetUi(presenter = EpisodeSheetPresenter::class, parentScope = ActivityScope::class), the processor emits EpisodeSheetUiBinding.kt. The
file is structurally identical to a @ScreenUi binding. The two differences are the return type (SheetContent instead of ScreenContent) and the absence of modifier
forwarding in the content lambda.
Composable signature requirement¶
The signature requirement matches @ScreenUi: @Composable fun <Name>(presenter: <PresenterType>, modifier: Modifier = Modifier). Only presenter is forwarded by the
generated code. The modifier parameter exists so the composable can still be called from preview code.
The generated wrapper does not pass a modifier because SheetContent.content is typed as (SheetChild) -> Unit. A modal overlay decides its own layout (typically inside
a ModalBottomSheet) in the composable body, not at the call site.
@TabUi¶
Marks a @Composable function as the Android renderer for one tab pager page. Parallel to @ScreenUi, but the predicate matches a TabChild<*> instead of a
ScreenDestination<*>.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
public annotation class TabUi(
val presenter: KClass<*>,
val parentScope: KClass<*>,
)
When to use @TabUi¶
Pair @TabUi with a tab pager presenter that exposes children as TabChild<*> instances rather than as ScreenDestination<*> instances. Tv Maniac's home shell uses this
shape: the home pager hosts a fixed set of tab pages (Discover, Library, Progress, Profile), each backed by a TabChild-wrapped tab presenter, and renders them through a
Set<ScreenContent> multibinding.
The annotation exists because @ScreenUi's generated predicate ((it as? ScreenDestination<*>)?.presenter is FooPresenter) does not match a TabChild<*>. @TabUi
generates the same ScreenContent multibinding entry but casts the child to TabChild<*> instead. From the host's point of view the registry is one set; the predicates
inside the set know how to recognise their own child type.
Parameters¶
The parameters have the same meaning as on @ScreenUi.
Minimal example¶
@Composable
@TabUi(presenter = DiscoverShowsPresenter::class, parentScope = ActivityScope::class)
public fun DiscoverScreen(
presenter: DiscoverShowsPresenter,
modifier: Modifier = Modifier,
) {
// ... compose UI here
}
Generated artifacts¶
For DiscoverScreen annotated @TabUi(presenter = DiscoverShowsPresenter::class, parentScope = ActivityScope::class), the processor emits one file
DiscoverScreenUiBinding.kt. The file is structurally identical to a @ScreenUi binding. The two differences are the cast inside the matches predicate
((it as? TabChild<*>)?.presenter is DiscoverShowsPresenter) and the cast inside the content lambda ((child as TabChild<*>).presenter as DiscoverShowsPresenter).
Composable signature requirement¶
The signature requirement matches @ScreenUi: @Composable fun <Name>(presenter: <PresenterType>, modifier: Modifier = Modifier). The generator forwards both presenter
and modifier to the composable.
@ChildPresenter¶
Marks a presenter class as a child presenter created and owned by a parent host presenter, such as a tab pager's pages. The processor generates a
<Presenter>ChildGraph graph extension plus a factory contributing to the parent host's scope.
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
public annotation class ChildPresenter(
val scope: KClass<*>,
val parentScope: KClass<*>,
)
When to use @ChildPresenter¶
Use @ChildPresenter when a presenter is constructed by another presenter rather than navigated to through a route. Tab pager pages, expanding-card sub-screens, and
similar sub-components fit this pattern. Routed destinations should use @NavDestination instead.
Without the annotation a parent presenter that exposes child presenters has to hand-write a @GraphExtension interface listing each child as a property plus a nested
@ContributesTo(parentScope) @GraphExtension.Factory that the parent presenter consumes. With the annotation the processor emits one graph extension per annotated child,
and the parent presenter consumes one factory per child.
Parameters¶
The scope parameter is the graph scope, used as the marker on the generated @GraphExtension. Multiple @ChildPresenter classes may share a scope; each gets its own
graph extension.
The parentScope parameter names the parent dependency injection scope hosting the generated factory. Typically the route class of the parent host (for example
ProgressRoot::class).
Minimal example¶
@Inject
@ChildPresenter(
scope = ProgressChildScope::class,
parentScope = ProgressRoot::class,
)
public class UpNextPresenter(
componentContext: ComponentContext,
// ... deps
) : ComponentContext by componentContext
Generated artifacts¶
For a presenter com.example.feature.upnext.UpNextPresenter annotated @ChildPresenter(scope = ProgressChildScope::class, parentScope = ProgressRoot::class), the
processor emits one file into com.example.feature.upnext.di:
UpNextChildGraph.kt: a@GraphExtension(ProgressChildScope::class) interface UpNextChildGraphexposingval upNextPresenter: UpNextPresenterand a nested@ContributesTo(ProgressRoot::class) @GraphExtension.Factory interface Factorywhose single functioncreateUpNextGraph(@Provides componentContext: ComponentContext)returns the graph.
The factory function name is unique per presenter (create<BaseName>Graph) so two child graphs that contribute to the same parent scope do not collide. The base name is
the presenter's simple name with the Presenter suffix stripped.
Parent presenter wiring¶
The parent presenter takes one factory parameter for each child:
@Inject
public class ProgressPresenter(
componentContext: ComponentContext,
upNextGraphFactory: UpNextChildGraph.Factory,
calendarGraphFactory: CalendarChildGraph.Factory,
) : ComponentContext by componentContext {
public val upNextPresenter: UpNextPresenter =
upNextGraphFactory.createUpNextGraph(childContext(key = "UpNext")).upNextPresenter
public val calendarPresenter: CalendarPresenter =
calendarGraphFactory.createCalendarGraph(childContext(key = "Calendar")).calendarPresenter
}
Embeddable / reusable components¶
parentScope decides which hosts can embed the child. Pointing it at a parent route (the example
above) ties the child to that one host. Pointing it at a shared ancestor scope makes the child
embeddable by every host below that scope, which is what lets a component live in its own module and
be reused.
Metro graph extensions resolve bindings from any ancestor scope. The scope chain runs
AppScope -> ActivityScope -> {tab roots} -> {child scopes}, each level a @GraphExtension whose
factory @ContributesTo the level above. A factory contributed to ActivityScope is therefore
visible to every tab root, stack screen, and child below it.
To make a component reusable, give it its own scope and set parentScope to ActivityScope:
@Inject
@ChildPresenter(
scope = FeaturedShowsComponentScope::class,
parentScope = ActivityScope::class,
)
public class FeaturedShowsPresenter(
componentContext: ComponentContext,
// ... deps available at ActivityScope or AppScope
) : ComponentContext by componentContext
Any host below ActivityScope then embeds it the same way a parent embeds a pager child:
@Inject
public class DiscoverPresenter(
componentContext: ComponentContext,
featuredGraphFactory: FeaturedShowsChildGraph.Factory,
) : ComponentContext by componentContext {
public val featuredPresenter: FeaturedShowsPresenter =
featuredGraphFactory.createFeaturedShowsGraph(childContext(key = "Featured")).featuredShowsPresenter
}
Nesting one embeddable component inside another works for free: the outer component's graph descends
from ActivityScope, so it can inject the inner component's ActivityScope-contributed factory.
The one constraint: an embeddable component may inject only bindings reachable at ActivityScope or
AppScope. Depending on a binding scoped to a specific tab root would compile in that root but fail
in any other host, which surfaces as an ordinary Metro missing-binding error at the embedding site,
not a silent failure. The generator is scope-agnostic, so the embeddable shape needs no annotation or
generator change: it is purely the parentScope you choose.
Validation¶
The processor reports a compile error if the annotated symbol is not a class.
Common pitfalls¶
- Forgetting
@Inject.@ChildPresenteronly tells the processor which graph to generate. The presenter still needs Metro's injection annotation so Metro creates instances of it. - Reusing a factory function name. Two child graphs that contribute to the same
parentScopecannot expose factory functions with the same name. The generator derives the function name from the presenter's simple name, so two child presenters in the same parent scope must have distinct simple names.
@AppRoot¶
Marks an @AssistedInject presenter implementation as the application's root host. The processor emits the @BindingContainer that wires the nested @AssistedFactory
to the bound interface at the parent scope.
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
public annotation class AppRoot(
val parentScope: KClass<*>,
)
When to use @AppRoot¶
Use @AppRoot on the activity-scope root presenter implementation. The root presenter is the top-level component the activity instantiates once and hands to the host
composable; it is not a destination in the navigation stack. Without the annotation the consumer has to hand-write a @BindingContainer @ContributesTo(parentScope)
object that takes the assisted factory and a ComponentContext and returns the bound interface. With the annotation the processor emits that file.
@AppRoot differs from @NavDestination in two ways. The root has no route, so the annotation does not take a route parameter. The root is bound to its public
interface at the parent scope (typically ActivityScope) rather than exposed through a @GraphExtension, so the generated artifact is a binding container, not a graph
plus a destination binding.
Parameters¶
The parentScope parameter names the dependency injection scope hosting the generated binding. Typically ActivityScope::class in the consumer project.
Minimal example¶
@AppRoot(parentScope = ActivityScope::class)
@AssistedInject
public class DefaultRootPresenter(
@Assisted componentContext: ComponentContext,
private val navigator: Navigator,
// ... more deps
) : RootPresenter, ComponentContext by componentContext {
@AssistedFactory
public fun interface Factory {
public fun create(componentContext: ComponentContext): DefaultRootPresenter
}
}
Generated artifacts¶
For an implementation com.example.app.presenter.DefaultRootPresenter annotated @AppRoot(parentScope = ActivityScope::class) and implementing RootPresenter, the
processor emits one file into com.example.app.presenter.di:
RootPresenterBindingContainer.kt: a@BindingContainer @ContributesTo(parentScope) objectwhose@Provides @SingleIn(parentScope)function takes aComponentContextand the nestedFactory, and returns the bound interface (RootPresenterin this case). The function body invokes the factory's single function with the suppliedComponentContext.
The output replaces the hand-written binding container the consumer would otherwise have to keep in sync with the implementation's factory function name and the bound interface name.
Bound interface inference¶
The processor reads the implementation's supertypes in declaration order and picks the first non-marker interface as the bound type. Decompose's ComponentContext
(commonly used as a delegate via ComponentContext by componentContext) and Kotlin's implicit Any are filtered out. Implementations that extend more than one
non-marker interface are rejected at compile time.
Validation¶
The processor reports a compile error if any of the following hold:
- The annotated symbol is not a class.
- The class does not carry
@AssistedInject. - The class does not declare a nested
@AssistedFactoryinterface. - The nested factory does not declare exactly one function.
- The class extends zero or more than one non-marker interface.
Common pitfalls¶
- Forgetting the nested factory.
@AppRootrequires@AssistedInjectplus a nested@AssistedFactory. The factory's single function must take aComponentContextand return the implementation type, mirroring how the consumer would have invoked the assisted factory by hand. - Multiple non-marker supertypes. The bound type is inferred from the supertype list. If the implementation extends two interfaces (for example a presenter contract and an extra interface), the processor cannot pick one and reports a compile error. Pick the bound type explicitly by removing the second interface, or merge the two contracts into one.
@AppRootUi¶
Marks the host @Composable function as the application's root UI. The processor generates a provider interface declaring one property for each non-modifier parameter
on the composable plus a Composable extension that invokes the composable using the receiver's properties.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
public annotation class AppRootUi(
val presenter: KClass<*>,
val parentScope: KClass<*>,
)
When to use @AppRootUi¶
Pair @AppRootUi with @AppRoot on the matching presenter implementation. The composable is the activity's top-level Compose entry point: it receives the root
presenter plus any multibinding sets the host needs (Set<ScreenContent>, Set<SheetContent>, and so on), and renders every screen the navigation system pushes
underneath it.
The composable is not part of the Set<ScreenContent> multibinding the navigation system iterates. It is the host that provides that set to its descendants. @ScreenUi
does not apply for that reason. @AppRootUi exists so the codegen can emit a provider interface keyed off the composable's parameter list, which lets the activity
graph extend that interface and the activity render the host with one call.
Parameters¶
The presenter parameter names the root presenter type. The processor reads the composable's parameters in declaration order, skips a final modifier: Modifier
parameter, and asserts that the first remaining parameter type equals presenter. Mismatch is a compile error.
The parentScope parameter names the dependency injection scope hosting the generated artifacts. Typically ActivityScope::class.
Minimal example¶
@AppRootUi(presenter = RootPresenter::class, parentScope = ActivityScope::class)
@Composable
public fun RootScreen(
rootPresenter: RootPresenter,
screenContents: Set<ScreenContent>,
sheetContents: Set<SheetContent>,
modifier: Modifier = Modifier,
) {
// ... compose UI here, including the navigation host
}
Generated artifacts¶
For a composable com.example.app.ui.RootScreen annotated @AppRootUi(presenter = RootPresenter::class, parentScope = ActivityScope::class), the processor emits one
file into com.example.app.ui.di:
RootScreenAppRootUiBinding.ktcontains two declarations:- An
AppRootProviderinterface declaring onevalfor each non-modifier parameter on the annotated composable. - A
@Composable AppRootProvider.AppRootContent(modifier: Modifier)extension that invokes the composable using the receiver's properties.
The consumer makes its activity-scope @DependencyGraph extend AppRootProvider, then calls graph.AppRootContent() from the activity. The activity call site
collapses from one argument per dependency to one extension call.
Composable signature requirement¶
The annotated function must:
- Be
@Composable. - Declare at least one non-modifier parameter.
- Declare its first non-modifier parameter as the
presentertype.
The processor reads the parameter list in order and skips any parameter named modifier whose type is androidx.compose.ui.Modifier. Every other parameter becomes a
property on the generated AppRootProvider interface.
Validation¶
The processor reports a compile error if any of the following hold:
- The annotated symbol is not a function.
- The function lives in the default (empty) package.
- The function has no non-modifier parameters.
- The first non-modifier parameter type does not equal
presenter. - More than one
@AppRootUiis declared in the same compilation round.
Common pitfalls¶
- Activity graph does not extend
AppRootProvider. The generated extension is onAppRootProvider, not on the consumer's graph type directly. The consumer must declare: AppRootProvideron its@DependencyGraphinterface so the extension resolves at the call site. - Adding a parameter without updating the graph. Each non-modifier parameter becomes a
valon the generated interface. If the activity graph already exposes a property of the same name and type, the new field is satisfied automatically. Otherwise the consumer needs to add the property, typically as a Metro multibinding or injection point.
DestinationKind¶
DestinationKind is the enum passed to @NavDestination(kind = ...). It picks the role the destination plays at runtime.
SCREEN: a screen pushed onto the navigation stack. Tapping the system back button pops it off and returns to the previous destination.OVERLAY: a modal overlay (sheet, dialog, or menu) that appears on top of the current screen without affecting the back stack. Dismissing the overlay returns to the screen that was visible underneath.TAB_ROOT: the destination shown when the user selects a top level tab. Each tab anchors its own back stack and persists across tab switches.
The SCREEN and OVERLAY outputs are structurally identical; they differ only in the NavDestination subclass the binding contributes. The consumer's navigator
inspects that subclass at runtime to choose between stack and overlay routing.
Required consumer primitives¶
The generated code references fully qualified names that the consumer project must provide. These are hardcoded in the processor's util/External.kt, grouped below by
the role each plays.
The destination types pull from com.thomaskioko.tvmaniac.navigation:
BaseRoute: sealed parent of every routable target.NavRoute: supertype for routes that map to navigation stack screens or overlays.NavRoot: supertype for routes that map to top level tab anchors.NavDestination: sealed factory family with nestedScreen,Overlay,TabRootsubclasses. Codegen emits one of these for each annotated presenter.NavRouteBinding: Metro multibinding entry forNavRoutepolymorphic serialization.NavRootBinding: Metro multibinding entry forNavRootpolymorphic serialization.RootChild: Decompose child marker; the return type of everyNavDestinationfactory lambda.ScreenDestination: generic root wrapper used byScreenandOverlayfactories.SheetChild,SheetDestination: used by overlay rendering at the slot host. The consumer typically rewraps aRootChildfrom anOverlayfactory into aSheetDestinationat the slot host.
Tabs additionally pull TabChild from com.thomaskioko.tvmaniac.home.nav. TabChild is the generic tab wrapper used by TabRoot factory lambdas.
com.thomaskioko.tvmaniac.core.base.ActivityScope is the default parent scope referenced by the generated binding contributions.
The Android UI renderer types live under com.thomaskioko.tvmaniac.navigation.ui. ScreenContent carries a matches predicate and a @Composable (RootChild, Modifier)
-> Unit content lambda. SheetContent does the same for SheetChild. The bindings generated for @ScreenUi and @SheetUi also reference
androidx.compose.ui.Modifier because the screen variant forwards a modifier into the composable.
@AppRoot references one additional Metro primitive, dev.zacsweers.metro.SingleIn, used to scope the generated provider to the parent scope. @AppRootUi emits a
top-level @Composable extension and so references androidx.compose.runtime.Composable and androidx.compose.ui.Modifier directly. These three names are also
hardcoded constants in util/External.kt and have matching test stubs.
The processor is opinionated about these names. Consumers other than Tv Maniac would need to adjust util/External.kt to match their navigation primitives. See
architecture/consumer-contract.md for why these are constants and what a fork would change.
Migration from earlier versions¶
Earlier releases of this module shipped @NavScreen, @TabScreen, and @NavSheet as separate annotations. They have been replaced by the unified @NavDestination(kind
= ...) API:
@NavScreenbecomes@NavDestination(kind = DestinationKind.SCREEN).@TabScreenbecomes@NavDestination(kind = DestinationKind.TAB_ROOT).@NavSheetbecomes@NavDestination(kind = DestinationKind.OVERLAY).
See the CHANGELOG for the version that introduced the change.