Back-button / ESC / Swipe-back Behaviour
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+.
Spec — Backbutton / ESC / swipeback behaviour
Applicability
All Koder *pps*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
- *ack 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.
- *nly 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.
- *efault 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). - *verlays pop first.*If a dialog, bottom sheet, drawer, or modal is
open, back closes the overlay before popping the Navigator.
- *ptional 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. - *ever 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 *xplicitly 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 in
KoderBackScopetriggers 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 driftprone across the Stack (theme, safearea, 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 rereading it — consistent with the SDKfirst 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