Back-button / ESC / Swipe-back Behaviour
specs/navigation/back-behavior.kmd
Back (Android <, iOS swipe, ESC desktop, browser back) **sempre** volta tela por tela na pilha do Navigator; só minimiza/fecha no root. Implementação canônica: KoderBackScope(enableSystemExitAtRoot: true) em engines/sdk/koder_kit v0.4.0+.
When this pattern applies
Primary triggers
- Implementar navegação ou comportamento do botão voltar / ESC em qualquer app Koder
All triggers
- Implementar navegação ou comportamento do botão voltar / ESC em qualquer app Koder
- Configurar pilha de Navigator em app Flutter Koder
- Resolver bug de back que minimiza app antes do root
Specification body
Spec — Back-button / ESC / swipe-back behaviour
Applicability
All Koder apps with any in-app navigation: mobile (Android back gesture, iOS edge swipe), desktop (Escape key, window close), web (browser back, Esc), TV (remote back). Not applicable to CLIs.
Required behaviour
- Back always pops the in-app Navigator stack first. Android "<" / gesture back, iOS edge-swipe, Desktop Escape, browser back — all honour the app's own history before any system-level action.
- Only at the root screen (Navigator stack empty) does back trigger the default system action: minimise to Home (Android), dismiss tab (browser), exit app (desktop window close), etc.
- Default at the root is "absorb", not "exit". The user staying in the
app when they tap back at the home screen is not a bug — it is explicit.
Apps that want the classic "press back at root → minimise" behaviour opt
in via
enableSystemExitAtRoot: true(see SDK widget below). - Overlays pop first. If a dialog, bottom sheet, drawer, or modal is open, back closes the overlay before popping the Navigator.
- Optional confirm gate. Screens with unsaved state may intercept with
a confirm dialog via the
onWillPopcallback; returningfalseblocks the back entirely (user stays put), returningtrueproceeds with the default pop/exit chain. - Never minimise from a deep screen. The Koder Pass QR-scanner bug — tap back, app minimises to Home instead of returning to the vault list — is explicitly forbidden by this spec.
Platform primitives
| Platform | API / gesture | Notes |
|---|---|---|
| Android native (API 33+) | OnBackPressedDispatcher + OnBackPressedCallback(true) |
Predictive-back compatible; the callback runs before the activity is finished |
| Android native (compat) | onBackPressed() override |
Deprecated API 33+; still works |
| iOS native | UINavigationController.popViewController + interactivePopGestureRecognizer |
Default behaviour of UINavigationController already matches the spec |
| Flutter | PopScope(canPop: false, onPopInvokedWithResult:) |
Covers Android back, iOS swipe, browser back on web |
| Flutter | Navigator.maybeOf(context)?.maybePop() |
Pops if there's a route; returns false if at root |
| Desktop (all via Flutter) | CallbackShortcuts + LogicalKeyboardKey.escape |
No system back — Escape is the convention |
| Web (Flutter web) | Browser back covered by PopScope via Flutter's routing |
Escape via same CallbackShortcuts mechanism |
| Web (vanilla JS) | history.pushState + popstate event + keydown ESC |
koder_web_kit (planned) will encapsulate |
| Flutter (system exit) | SystemNavigator.pop() |
Call this at the root screen to minimise |
SDK widget (approved implementation)
| Platform | SDK | Widget |
|---|---|---|
| Flutter (Android / iOS / Linux / macOS / Windows / Web) | engines/sdk/koder_kit v0.4.0+ |
KoderBackScope(child: …, onWillPop?, enableSystemExitAtRoot?) |
| Web (vanilla) | engines/sdk/koder_web_kit (planned) |
<koder-back-scope> custom element with matching props |
| Native Android Kotlin | koder-kit-android (deferred) |
@Composable fun KoderBackScope with matching API |
Flutter usage
// Typical app shell — wrap the MaterialApp root once:
runApp(
KoderApp(
config: KoderConfig(slug: 'pass', version: '1.2.3'),
home: const KoderBackScope(
enableSystemExitAtRoot: true, // minimise only at the true root
child: HomeScreen(),
),
),
);
// Per-screen with unsaved-changes confirm:
KoderBackScope(
onWillPop: (context) async => await showDiscardDialog(context),
child: const EditorScreen(),
);
Native Android Kotlin pattern (until the Kotlin SDK ships)
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (navController.popBackStack()) return
// At root — decide: finish() to minimise, or absorb.
finish()
}
})
setContent { … }
}
}
Deterministic audit checks
/k-housekeep audit (ticket engines/sdk/koder_kit#003 extended):
- Any Flutter app with
koder_kitinpubspec.yamlwhoseKoderApp.homeis not wrapped inKoderBackScopetriggers a warning. - Any Flutter screen that calls
SystemNavigator.popdirectly (bypassingKoderBackScope) triggers a warning. - Any landing / web app loading
koder-web-kit.jswithout<koder-back-scope>on interactive screens (determined by presence of ≥2 SPA-style page transitions) triggers a warning.
Rationale
The /k-housekeep audit of February 2026 surfaced this pattern as one of
the three most drift-prone across the Stack (theme, safe-area, back).
dev/eye had it fixed only after the status bar clipping was visible in a
screenshot; Koder Pass still has the bug in production (QR scanner
minimises instead of popping). Every Koder app has needed it, most
implemented it wrong or partially.
The SDK widget collapses the N platform-specific APIs into one symbol and
one opt-in flag. Apps adopting KoderBackScope inherit the spec's
compliance without ever re-reading it — consistent with the SDK-first
policy at policies/sdk-first.kmd.
Canonical reference
engines/sdk/koder_kit/lib/src/back_scope.dart — Flutter reference.
engines/sdk/koder_kit/test/back_scope_test.dart — 5 widget tests locking the
contract.
Related specs
../themes/light-dark.kmd../app-layout/safe-area.kmd../errors/user-facing-messages.kmd../koder-app/behaviors.kmd
References
engines/sdk/koder_kitpolicies/sdk-first.kmd