ADR-0037: Onboarding Completion Tracking and Re-trigger Conditions
DateMarch 1, 2026
CategoryOnboarding & Features
Tagsonboardingsettings-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 value | Meaning |
|---|
false | Onboarding not yet completed or skipped |
true | Onboarding 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:
| Setting | Written when | Persists on skip? |
|---|
| Display name | User enters name on Welcome screen | Yes, if entered |
| Timezone override | User confirms timezone on Ready screen | Yes, if reached |
| AI opt-in flag | User chooses on AI Opt-In screen | Yes, if reached |
| Model download state | Model finishes downloading | Yes (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.