Back-button / ESC / Swipe-back Behaviour

navigation 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

All triggers

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

  1. 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.
  2. 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.
  3. 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).
  4. Overlays pop first. If a dialog, bottom sheet, drawer, or modal is open, back closes the overlay before popping the Navigator.
  5. Optional confirm gate. Screens with unsaved state may intercept with a confirm dialog via the onWillPop callback; returning false blocks the back entirely (user stays put), returning true proceeds with the default pop/exit chain.
  6. 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):

  1. Any Flutter app with koder_kit in pubspec.yaml whose KoderApp.home is not wrapped in KoderBackScope triggers a warning.
  2. Any Flutter screen that calls SystemNavigator.pop directly (bypassing KoderBackScope) triggers a warning.
  3. Any landing / web app loading koder-web-kit.js without <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.

  • ../themes/light-dark.kmd
  • ../app-layout/safe-area.kmd
  • ../errors/user-facing-messages.kmd
  • ../koder-app/behaviors.kmd

References