All Decisions

ADR-0037: Onboarding Completion Tracking and Re-trigger Conditions

DateMarch 1, 2026
CategoryOnboarding & Features
Tags
onboardingsettings-persistence

Context

  • The onboarding flow (ADR-0034) is a ~6-screen first-run experience that can be completed or skipped.
  • ADR-0027 already defines an onboarding_completed boolean in Jetpack DataStore, read via SettingsRepository.onboardingCompleted: Flow<Boolean>.
  • The onboarding screens are reusable composables that also serve as a tutorial replay from Settings (ADR-0034 §5).
  • Settings that are collected during onboarding (display name, timezone, AI opt-in) must persist regardless of how onboarding ends.
  • Skip is a hard dismissal — the first-run onboarding gate never triggers again (ADR-0034 §4).
  • Re-triggering onboarding for novel scenarios (e.g., major app updates) is explicitly deferred.

Constraints:

  • State must survive app kills, process death, and device restarts.
  • The gate check must be fast and synchronous-safe at startup to avoid flashing the main UI before redirecting to onboarding.
  • Must be compatible with the existing DataStore-based settings architecture (ADR-0027).

Decision

1. Completion State: Single Boolean in DataStore

We will track onboarding completion with the existing onboarding_completed boolean key in Preferences DataStore (ADR-0027).

State valueMeaning
falseOnboarding not yet completed or skipped
trueOnboarding completed (reached last screen) OR explicitly skipped

There is no distinction between "completed" and "skipped" in the persistence layer. Both result in onboarding_completed = true.

2. Startup Gate

MainActivity (or a root-level composable / splash logic) observes SettingsRepository.onboardingCompleted at launch:

// In the root composable or NavHost setup
val onboardingCompleted by settingsRepository.onboardingCompleted
    .collectAsState(initial = null)  // null = still loading

when (onboardingCompleted) {
    null -> SplashScreen()  // or loading indicator while DataStore initializes
    false -> OnboardingNavGraph(...)
    true -> MainNavGraph(...)
}

The null → loading state prevents the main UI from flashing before the DataStore read completes (typically <50ms on warm start, but can be slower on first cold start).

3. Settings Collected During Onboarding

Settings written during the onboarding flow persist immediately via SettingsRepository, independent of onboarding completion state:

SettingWritten whenPersists on skip?
Display nameUser enters name on Welcome screenYes, if entered
Timezone overrideUser confirms timezone on Ready screenYes, if reached
AI opt-in flagUser chooses on AI Opt-In screenYes, if reached
Model download stateModel finishes downloadingYes (async)

If the user skips before reaching a particular screen, the corresponding setting retains its default value. The user can configure it later in Settings.

4. Tutorial Replay (No State Mutation)

When the user replays the onboarding from Settings ("Replay Tutorial"):

  • The same composable screens are rendered with tutorialMode = true (ADR-0034 §2).
  • No DataStore writes occur. The tutorial is read-only — it does not modify display name, timezone, AI opt-in, or onboarding_completed.
  • The tutorial can be exited at any point via a "Close" button without side effects.

5. Re-trigger Conditions — Deferred

Automatic re-triggering of the onboarding flow (e.g., after a major version update, after a data reset, or after a long absence) is explicitly deferred to a future ADR.

The current architecture supports future re-triggering by:

  • Resetting onboarding_completed to false — this is sufficient to re-trigger the gate.
  • Adding a last_completed_onboarding_version: Int key to DataStore if version-aware re-triggering is needed later.

These keys are not created now; the decision on when and why to re-trigger onboarding is deferred until there is user testing data or a major UX change that warrants it.

6. Data Reset Behavior

When the user performs "Clear all data" from Settings (SettingsRepository.clearAll()):

  • onboarding_completed resets to false (its default).
  • On next app launch, the onboarding gate triggers again, presenting the full first-run flow.
  • This is intentional: a full data reset implies a "fresh start" that should include re-onboarding.

Rationale

  • Single boolean, no granular step tracking. Tracking which onboarding step the user last completed adds complexity (partial-completion state machine, resume logic) for minimal benefit. The flow is short (~10 screens, ~3–5 minutes); users who re-enter after skip or crash simply start from the beginning. The tutorial replay covers the "I want to see it again" use case.
  • DataStore over SharedPreferences. ADR-0027 standardized on DataStore for all preferences. The onboarding_completed key already exists there. Using SharedPreferences would create a second preferences store, violating the single-source-of-truth principle.
  • Immediate setting writes. Writing settings as the user progresses (not batching until completion) ensures that a user who partially completes onboarding retains any settings they did configure. This avoids "lost work" frustration.
  • Deferred re-trigger. There is no current use case for automatic re-triggering. Designing a re-trigger mechanism without user testing data risks over-engineering. The architecture supports adding it later with a single DataStore key addition.

Consequences

  • Positive:
    • Minimal state management — one boolean, checked once at startup.
    • No partial-completion state machine to maintain.
    • Tutorial replay satisfies the "show me again" need without touching onboarding state.
    • Full data reset naturally re-triggers onboarding.
  • Negative:
    • Users who are interrupted mid-onboarding (app killed, phone restart) restart the flow from the beginning. Acceptable given the flow's short length.
    • No distinction between "completed" and "skipped" — analytics cannot differentiate. If needed, a separate analytics event can be added later (ADR-0039).
  • Follow-up:
    • If user testing reveals frequent mid-flow abandonment, consider adding a last_onboarding_ step: Int key to DataStore for resume support. This is a non-breaking addition.
    • A future ADR may define version-aware re-triggering for major UX changes.

Alternatives Considered

  • Step-level completion tracking (e.g., onboarding_last_step: Int) — rejected for now. Adds complexity without clear benefit for a ~6-screen flow. Can be added later if needed.
  • Enum-based state (NOT_STARTED, IN_PROGRESS, COMPLETED, SKIPPED) — rejected. Over-engineered for the current requirements. The boolean is sufficient and aligns with the existing DataStore schema.
  • Room-based onboarding state — rejected. Onboarding state is a simple flag, not relational data. DataStore is the correct home (ADR-0027).

Notes

  • Related ADRs: ADR-0027 (settings persistence), ADR-0034 (onboarding flow structure), ADR-0035 (model download timing).
  • The onboarding_completed key is already implemented in DataStoreSettingsRepository and exposed via SettingsRepository.onboardingCompleted. No new keys are introduced by this ADR.