All Decisions

ADR-0025: UI Package Restructuring

DateFebruary 23, 2026
CategoryUI & Navigation
Tags
navigationcompose-ui

Context

The UI layer has grown organically across two parallel package trees that are now inconsistent with each other:

  1. designsystem/ — holds token definitions (tokens/), the Material theme (theme/), and a set of structural shell components (components/), such as AppTopBar, AppBottomNavigation, AppDrawerContent, PrimaryButton, BottomLinedTitleField, and NavigationItems.
  2. ui/ — holds feature screens and their co-located components, but also a flat ui/components/ folder containing domain-aware shared components (CategoryDropdown, ContextDropdown, Pills, MetadataComponents, etc.).
  3. navigation/ — sits at the top level of the package: NavGraph.kt (the monolithic locusFlowNavGraph extension function) and NavigationRoutes.kt.

This fragmentation creates three problems:

  • There is no single, obvious home for a shared component; engineers choose between designsystem/components/ and ui/components/ arbitrarily.
  • All navigation wiring for every screen lives in one file (NavGraph.kt). As the number of navigatable destinations grows, this file becomes a maintenance bottleneck and makes feature ownership harder to trace.
  • Some features (inbox, reflection, reference, morning) have no State file and no per-feature navigation fragment, making them structurally inconsistent with heavier features like edititem, processing, nextactions, and someday.

A uniform, feature-first UI package layout is needed before the feature count grows further.

Decision

We will reorganise all UI source files under a single ui/ root package with four canonical subdirectories — navigation, features, components, and theme — and we will dissolve the designsystem/ package into those subdirectories.

Target package layout

ui/ ├── navigation/ │ ├── NavGraph.kt (orchestrator — calls feature extensions only) │ └── NavigationRoutes.kt │ ├── features/ │ ├── edititem/ │ │ ├── EditItemScreen.kt │ │ ├── EditItemState.kt │ │ ├── EditItemViewModel.kt │ │ ├── EditItemNavigation.kt ← new NavGraphBuilder extension │ │ └── components/ │ │ ├── AnimatedContentField.kt │ │ ├── ClassificationRow.kt │ │ └── EditItemFormContent.kt │ ├── inbox/ │ │ ├── InboxScreen.kt │ │ ├── InboxViewModel.kt │ │ └── InboxNavigation.kt ← new NavGraphBuilder extension │ ├── llm/ │ │ ├── ModelDownloadScreen.kt │ │ ├── ModelDownloadViewModel.kt │ │ ├── ModelStatusBanner.kt │ │ └── LlmNavigation.kt ← new NavGraphBuilder extension │ ├── morning/ │ │ ├── MorningBriefScreen.kt │ │ ├── MorningBriefState.kt │ │ ├── MorningBriefViewModel.kt │ │ └── MorningBriefNavigation.kt ← new NavGraphBuilder extension │ ├── nextactions/ │ │ ├── NextActionsScreen.kt │ │ ├── NextActionsViewModel.kt │ │ ├── NextActionsNavigation.kt ← new NavGraphBuilder extension │ │ └── components/ │ │ ├── FilterSortControls.kt │ │ ├── NextActionsFilterDropdown.kt │ │ ├── NextActionsSearchBar.kt │ │ ├── ProcessedItemCard.kt │ │ ├── ScrollableActionsList.kt │ │ └── SearchFilterRow.kt │ ├── processing/ │ │ ├── ProcessingScreen.kt │ │ ├── ProcessingState.kt │ │ ├── ProcessingViewModel.kt │ │ ├── ProcessingNavigation.kt ← new NavGraphBuilder extension │ │ └── components/ │ │ ├── ActionableBoxes.kt │ │ ├── ButtonsRow.kt │ │ ├── EmptyProcessingState.kt │ │ ├── IconTextButton.kt │ │ ├── InnerCard.kt │ │ ├── NonActionableBoxes.kt │ │ ├── ProcessingCard.kt │ │ ├── ProcessingOption.kt │ │ └── SwipeableCardStack.kt │ ├── reference/ │ │ ├── ReferenceScreen.kt │ │ ├── ReferenceViewModel.kt │ │ └── ReferenceNavigation.kt ← new NavGraphBuilder extension │ ├── reflection/ │ │ ├── DailyReflectionScreen.kt │ │ ├── DailyReflectionViewModel.kt │ │ └── DailyReflectionNavigation.kt ← new NavGraphBuilder extension │ └── someday/ │ ├── SomedayMaybeScreen.kt │ ├── SomedayMaybeViewModel.kt │ ├── SomedayMaybeNavigation.kt ← new NavGraphBuilder extension │ └── components/ │ ├── SomedayFilterDropdown.kt │ ├── SomedayFilterSortControls.kt │ ├── SomedayItemCard.kt │ ├── SomedayScrollableList.kt │ ├── SomedaySearchBar.kt │ └── SomedaySearchFilterRow.kt │ ├── components/ (all shared / cross-feature composables) │ │ ── From designsystem/components/ │ ├── AppBottomNavigation.kt │ ├── AppDrawerContent.kt │ ├── AppTopBar.kt │ ├── BottomLinedTitleField.kt │ ├── NavigationItems.kt │ ├── PrimaryButton.kt │ │ ── From ui/components/ │ ├── CategoryDropdown.kt │ ├── ContextDropdown.kt │ ├── EditableTagDropdown.kt │ ├── MetadataComponents.kt │ ├── MultiSelectDropdown.kt │ ├── Pills.kt │ ├── PlaceholderScreen.kt │ ├── SingleSelectDropdown.kt │ └── TagDropdown.kt │ └── theme/ (Material theme + all design tokens) │ ── From designsystem/theme/ ├── AppTheme.kt │ ── From designsystem/tokens/ ├── ColorTokens.kt ├── Motion.kt ├── ShapeTokens.kt ├── Spacing.kt └── TypographyTokens.kt

Navigation decomposition

Each composable(...) block currently inside NavGraphBuilder.locusFlowNavGraph is extracted into a NavGraphBuilder extension function in a *Navigation.kt file co-located with the owning feature package. The locusFlowNavGraph body is then reduced to a sequence of calls to those extensions.

Example — EditItemNavigation.kt:

// ui/features/edititem/EditItemNavigation.kt
fun NavGraphBuilder.editItemNavigation(navController: NavHostController) {
    composable(
        route = NavigationRoutes.EDIT_ITEM,
        arguments = listOf(navArgument("itemId") { type = NavType.LongType }),
    ) {
        val viewModel: EditItemViewModel = hiltViewModel()
        EditItemScreen(
            viewModel = viewModel,
            onNavigateBack = { navController.popBackStack() },
            onItemDeleted = { navController.popBackStack() },
        )
    }
}

Resulting locusFlowNavGraph body:

fun NavGraphBuilder.locusFlowNavGraph(navController: NavHostController) {
    inboxNavigation(navController)
    nextActionsNavigation(navController)
    processingNavigation(navController)
    dailyReflectionNavigation(navController)
    somedayMaybeNavigation(navController)
    referenceNavigation(navController)
    morningBriefNavigation(navController)
    llmNavigation(navController)
    editItemNavigation(navController)
    // placeholder destinations remain here until promoted to features
    composable(NavigationRoutes.VISION) { PlaceholderScreen("Personal Vision") }
    composable(NavigationRoutes.SETTINGS) { PlaceholderScreen("Settings Screen") }
    composable(NavigationRoutes.PROJECTS) { PlaceholderScreen("Projects Screen") }
}

Scope boundaries

  • File names are preserved exactly as they are today; only the package declarations and imports change.
  • The designsystem/ package is fully dissolved; no files remain there after the migration.
  • The existing navigation/ top-level package becomes ui/navigation/.
  • Test and AndroidTest source sets follow the same structural change under their respective source roots.
  • No logic, public APIs, or signatures are changed during this refactor.

Rationale

  • Single shared-component home. Merging designsystem/components/ into ui/components/ eliminates the ambiguous dual-home problem. The distinction between "design primitive" and "domain-aware shared component" is captured by naming and documentation, not by separate packages.
  • Token co-location with theme. Tokens only make sense in the context of the theme that consumes them. Placing both under ui/theme/ makes the layering explicit without adding package depth.
  • Feature-first navigation ownership. Extracting each composable block into a *Navigation.kt file co-located with the feature means that a developer working on a feature can find and modify all of its routing in one place. It also makes NavGraph.kt stable and small, reducing merge conflicts.
  • Consistent feature structure. Requiring every feature to follow the same layout (Screen, ViewModel, State, Navigation, components/) makes onboarding and code review predictable.
  • Zero behavioural risk. Because only packages and imports change, the refactor is mechanical and can be reviewed purely for correctness of references.

Consequences

  • Positive: All UI code lives under a single ui/ root, with clear sub-trees for navigation, features, shared components, and the theme.
  • Positive: NavGraph.kt becomes a thin orchestrator; adding a new screen no longer requires editing it beyond adding one call.
  • Positive: Lint, Detekt, and module-boundary rules can now be targeted at ui/features/** vs ui/components/** vs ui/theme/** with meaningful semantics.
  • Negative: The rename is a large mechanical diff across all files in the UI layer; every import of designsystem is updated. Git history is preserved as long as moves are performed with git mv.
  • Follow-up: Once placeholder screens (Vision, Settings, Projects) are implemented, they will each receive their own feature package and a *Navigation.kt following this ADR.
  • Follow-up: Detekt module-boundary rules should be updated to reflect the new package layout (separate ADR not required, tracked in the project roadmap).

Alternatives Considered

  • Keep designsystem/ as-is and only reorganise ui/ — Rejected. This retains the ambiguity of two component homes and still requires developers to decide which package to use for each new shared composable.
  • Full multi-module split (:feature:inbox, :ui:components, etc.) — Rejected at this stage. Module boundaries add build complexity and Hilt wiring overhead that is not justified by the current codebase size. A package-level restructure achieves the same ownership clarity and leaves the door open for modules later (ADR-0003, ADR-0005 philosophy retained).
  • Monolithic ui/ flat layout (no feature subfolders) — Already the current state for some features; rejected because it does not scale beyond ~five screens without name-collision and discoverability problems.

Notes

  • Related ADRs: ADR-0003 (MVVM/Repository/UseCase layering), ADR-0006 (UI state modelling).
  • Migration should be done in a single atomic commit per feature to keep reviewable diffs, with a final commit updating NavGraph.kt.
  • The designsystem/ folder in commonMain/ (if any shared multiplatform tokens exist there) is out of scope and addressed separately.