Navigation 3 Migration Plan
Implementation branch: spike/nav3-about-runtime-only
All features remain runtimeOnly — app module does NOT compile against feature implementations. Navigation 3 v1.1.2, Koin 4.2.1.
PR 1 — Nav3 dependency + app:navigation key contracts
- Add
navigation3-runtime and kotlinx-serialization to app:navigation
- Add
AniTrendNavKey (open interface, non-sealed for cross-package subtyping)
- Add
NavCommand (Push, Pop)
- Add
NavigationDispatcher interface
app:navigation stays non-Compose — no compile plugin, no buildFeatures.compose
PR 2 — common:navigation
- New
:common:navigation module — the only Compose-bearing nav contract layer
FeatureNavEntryProvider — runtime feature registration seam
FeatureNavRegistry + FeatureNavEntryScope — Compose entry registration
FeatureNavEntryProviderRepository — abstraction for provider discovery
- Gets Compose naturally via
matchesCommonModule() → hasComposeSupport()
PR 3 — app runtime host foundation
AniTrendNavigationDispatcher — SharedFlow-based dispatcher impl
RuntimeFeatureNavRegistry — adapter: providers → Nav3 entries
KoinFeatureNavEntryProviderRepository — Koin getAll via bind
- Duplicate key detection (warning, not crash — idempotent install)
- Unit tests: registry registration, duplicate, missing key, multi-provider
- Process-death verified: kill + relaunch → falls back to
startKey
PR 4 — About production migration
AboutNavKey data object
AboutNavEntryProvider implemented in feature/about
- Koin binding:
factory { AboutNavEntryProvider() } bind FeatureNavEntryProvider::class
Nav3AwareProvider interface + startNav3OrActivity extension (tries Nav3 first, falls back to Activity)
FeatureProvider accepts nullable NavigationDispatcher via constructor
- Settings call site updated to
startNav3OrActivity
- Old Activity fallback preserved —
AboutScreen and AboutRouter.Provider intact
- Verified:
feature/about on runtimeClasspath, NOT compileClasspath
PR 5 — MainActivity Compose shell spike
MainComposeShellActivity (debug-only): ModalNavigationDrawer + Scaffold + TopAppBar + AniTrendNav3Host
- Hamburger icon opens drawer with navigation items
- Drawer calls
dispatcher.navigate() — not state-key changes
LegacyFragmentNavKey created for future Fragment compat
- Does NOT migrate all drawer destinations — shell and content are separate PRs
PR 6+ — feature-by-feature migrations
Each feature follows the same pattern:
- Create
XxxNavKey in app:navigation
- Create
XxxNavEntryProvider in the feature module (runtimeOnly)
- Koin:
factory { XxxNavEntryProvider() } bind FeatureNavEntryProvider::class
- Register entry in
AniTrendNav3Host entryProvider
- Verify runtimeOnly boundary
- Verify back navigation
- Verify process death
- Remove Activity fallback only after stabilization
Completed migrations:
| Feature |
NavKey |
Notes |
| Airing |
AiringNavKey |
Pure Compose screen |
| ImageViewer |
ImageViewerNavKey(imageSources, initialIndex) |
Serializable payload, download permission via rememberLauncherForActivityResult |
| Settings |
SettingsNavKey(destination) + SettingsDestination enum |
Wraps existing Compose NavHost; internal child navigation preserved |
Drawer destination keys created (content pending per-feature migration):
HomeNavKey, DiscoverNavKey, NewsNavKey, SocialNavKey, EpisodesNavKey, ReviewsNavKey, SuggestionsNavKey, AnimeListNavKey, MangaListNavKey, ForumsNavKey
PR 10 — External boundaries
DeepLinkMapper: resolves anilist.co URIs and anitrend:// scheme links to Nav3 keys
MediaNavKey(mediaId), ProfileNavKey(userId) for content deep links
- Notification intent → Nav3 key resolution
- Pattern: Activities stay for system entry points, translate input to Nav3 keys
Module Architecture (final)
app:navigation → pure contracts, non-Compose
common:navigation → Compose entry contribution (providers, registry, scopes)
app → runtime host (dispatcher, registry impl, NavDisplay)
feature:* → runtimeOnly, contribute via FeatureNavEntryProvider + Koin bind
Koin Multi-Provider Pattern
Use org.koin.dsl.bind (NOT org.koin.core.module.dsl.bind):
import org.koin.dsl.bind
module {
factory { AboutNavEntryProvider() } bind FeatureNavEntryProvider::class
factory { AiringNavEntryProvider() } bind FeatureNavEntryProvider::class
}
// App: koin.getAll<FeatureNavEntryProvider>()
Hard Rules
app:navigation must not depend on Compose
common:navigation depends on app:navigation
- Feature modules depend on
app:navigation + common:navigation
- App must not
implementation-depend on feature modules
- Feature modules remain
runtimeOnly from app
- Nav3 keys are immutable, small,
@Serializable, contain no framework objects
- Duplicate key registration warns, does not crash
- Keep old Activity/Fragment fallbacks until Nav3 path is process-death-verified
Branch
spike/nav3-about-runtime-only
Navigation 3 Migration Plan
Implementation branch:
spike/nav3-about-runtime-onlyAll features remain runtimeOnly — app module does NOT compile against feature implementations. Navigation 3 v1.1.2, Koin 4.2.1.
PR 1 — Nav3 dependency + app:navigation key contracts
navigation3-runtimeandkotlinx-serializationtoapp:navigationAniTrendNavKey(open interface, non-sealed for cross-package subtyping)NavCommand(Push, Pop)NavigationDispatcherinterfaceapp:navigationstays non-Compose — no compile plugin, no buildFeatures.composePR 2 — common:navigation
:common:navigationmodule — the only Compose-bearing nav contract layerFeatureNavEntryProvider— runtime feature registration seamFeatureNavRegistry+FeatureNavEntryScope— Compose entry registrationFeatureNavEntryProviderRepository— abstraction for provider discoverymatchesCommonModule()→hasComposeSupport()PR 3 — app runtime host foundation
AniTrendNavigationDispatcher— SharedFlow-based dispatcher implRuntimeFeatureNavRegistry— adapter: providers → Nav3 entriesKoinFeatureNavEntryProviderRepository— KoingetAllviabindstartKeyPR 4 — About production migration
AboutNavKeydata objectAboutNavEntryProviderimplemented infeature/aboutfactory { AboutNavEntryProvider() } bind FeatureNavEntryProvider::classNav3AwareProviderinterface +startNav3OrActivityextension (tries Nav3 first, falls back to Activity)FeatureProvideraccepts nullableNavigationDispatchervia constructorstartNav3OrActivityAboutScreenandAboutRouter.Providerintactfeature/abouton runtimeClasspath, NOT compileClasspathPR 5 — MainActivity Compose shell spike
MainComposeShellActivity(debug-only):ModalNavigationDrawer+Scaffold+TopAppBar+AniTrendNav3Hostdispatcher.navigate()— not state-key changesLegacyFragmentNavKeycreated for future Fragment compatPR 6+ — feature-by-feature migrations
Each feature follows the same pattern:
XxxNavKeyinapp:navigationXxxNavEntryProviderin the feature module (runtimeOnly)factory { XxxNavEntryProvider() } bind FeatureNavEntryProvider::classAniTrendNav3HostentryProviderCompleted migrations:
AiringNavKeyImageViewerNavKey(imageSources, initialIndex)rememberLauncherForActivityResultSettingsNavKey(destination)+SettingsDestinationenumNavHost; internal child navigation preservedDrawer destination keys created (content pending per-feature migration):
HomeNavKey,DiscoverNavKey,NewsNavKey,SocialNavKey,EpisodesNavKey,ReviewsNavKey,SuggestionsNavKey,AnimeListNavKey,MangaListNavKey,ForumsNavKeyPR 10 — External boundaries
DeepLinkMapper: resolvesanilist.coURIs andanitrend://scheme links to Nav3 keysMediaNavKey(mediaId),ProfileNavKey(userId)for content deep linksModule Architecture (final)
Koin Multi-Provider Pattern
Use
org.koin.dsl.bind(NOTorg.koin.core.module.dsl.bind):Hard Rules
app:navigationmust not depend on Composecommon:navigationdepends onapp:navigationapp:navigation+common:navigationimplementation-depend on feature modulesruntimeOnlyfrom app@Serializable, contain no framework objectsBranch
spike/nav3-about-runtime-only