App lifecycle (§ lifecycle)

A Day app can hook the moments its process moves through: launching, gaining and losing focus, going to and from the background, running low on memory, and terminating. Each phase maps to the host OS’s native app/activity delegate, when a backend receives the corresponding native callback. OS termination without a callback cannot deliver a handler; persist important state before termination.

use day::prelude::*;

fn main() {
    // Register before launch so `WillLaunch` / `DidLaunch` are captured.
    day::on_lifecycle(Lifecycle::DidLaunch, || println!("up and running"));
    day::on_lifecycle(Lifecycle::WillTerminate, || save_state()); // ⌘Q / Ctrl+Q / OS quit

    day::launch(app::res::window_options(), app::root);
}

Handlers run in registration order, inside a reactive batch, so a lifecycle handler that writes a Signal updates the UI just like a button callback. Register as many as you like per phase. Before WillTerminate handlers run, Day cancels pending main-loop tasks while reactive state is still alive. Handlers must finish shutdown synchronously; newly submitted tasks are not polled. A returning loop does the same at DidExit if necessary. See async shutdown.

The phases

PhaseWhen
WillLaunchBefore the window/UI exists; the first thing to run. Set up global state.
DidLaunchThe UI is mounted and laid out; the app is about to run.
DidBecomeActiveCame to the foreground and is the focused, interactive app.
WillResignActiveAbout to stop being active (an interruption, app switch, losing focus).
WillEnterForeground(mobile) About to return to the foreground.
DidEnterBackground(mobile) Left the foreground and is no longer visible. Persist state.
DidReceiveMemoryWarning(mobile) The system is low on memory. Drop caches.
WillTerminateAbout to terminate; the last chance to save.
DidExitThe run loop is over; nothing of the app runs after this. Day’s exit line follows the handlers.

WillLaunch and DidLaunch are emitted uniformly by day-core (reliable everywhere); the rest come from each backend’s native app/activity delegate.

The exit line

Once the DidExit handlers have run, Day logs one last line at info:

Clean exit for Day Rise v0.4.5 (debug) after 00:03:12. Memory usage: 48.21 MB

The name is the app’s (WindowOptions::app_name, else its title), the version is WindowOptions::version (the scaffold’s window() sets it to the crate’s CARGO_PKG_VERSION, and day::day_start! fills it in when it is unset), falling back to the DAY_APP_VERSION the day CLI sets on a launch; the profile is the build’s; the time is since day::launch; the memory is the process’s resident set as the platform counts it (the physical footprint on Apple platforms, /proc/self/statm on Linux and Android, the working set on Windows, the linear memory on the web). A handler registered for DidExit runs before the line, so it is the app’s last word:

day::on_lifecycle(Lifecycle::DidExit, || log::info!("bye"));

The line marks completion of Day’s exit handlers: the quit command, an orderly OS shutdown, or a page-exit callback when the browser delivers one. It precedes final native and thread-local destruction, so a later destructor failure can still crash the process after this line. A kill or earlier crash may prevent the line entirely. The memory figure covers this process, not separate WebKit helpers.

When to register

Register before day::launch (in main) to catch WillLaunch/DidLaunch. Registering later (inside your root builder, say) still catches everything from DidLaunch onward. On the mobile shells, the root builder is the natural registration point (there is no main you own); those handlers see DidLaunch and after.

Platform availability

Not every phase exists on every platform: a desktop app doesn’t enter the background or run out of memory the way a phone does. The universal phases (WillLaunch, DidLaunch, DidBecomeActive, WillResignActive, WillTerminate) are delivered by every backend. The background/foreground/memory phases are delivered only by the mobile backends (UIKit, Android, ArkUI).

PhaseAppKitGTKQtUIKitAndroidArkUIXAML
WillLaunch / DidLaunch✓✓✓✓✓✓✓
DidBecomeActive / WillResignActive✓✓✓✓✓✓✓
WillEnterForeground / DidEnterBackground———✓✓✓—
DidReceiveMemoryWarning———✓✓✓—
WillTerminate✓✓✓✓✓✓✓
DidExit✓✓✓✓✓✓✓

Native mapping: AppKit NSApplication notifications; UIKit UIApplicationDelegate; GTK window is-active + GApplication shutdown; Qt applicationStateChanged + aboutToQuit; Android Activity lifecycle (onResume/onPause/onStart/onStop/onTrimMemory/onDestroy); ArkUI’s entry UIAbility (onForeground/onBackground/onMemoryLevel/onDestroy, with the window stage’s ACTIVE/INACTIVE events for the active pair); XAML window WM_ACTIVATE/WM_CLOSE.

The web backend delivers WillLaunch, DidLaunch, the active pair, and DidExit (on pagehide, the page’s last moment).

DidExit is the last moment each backend has: where the native loop returns to day::launch (GTK, Qt, XAML) it is emitted there; where the process ends inside the loop it follows WillTerminate at the same native moment (AppKit’s and UIKit’s applicationWillTerminate, Android’s onDestroy, ArkUI’s onDestroy); on the web it is pagehide. A backend whose host owns the loop, so that Platform::run hands back at once with the app still to come (ArkUI, Android, the web), says so with Platform::RUN_ENDS_APP = false: day-core then leaves the phase to the host’s callback instead of treating the return as the exit.

Guarding platform-specific phases

Registering a handler for a phase the running backend doesn’t deliver is not an error: the handler is never called, and Day logs a one-time warning at launch:

day: an `on_lifecycle(DidEnterBackground)` handler was registered, but this backend never delivers
that phase, so it will not run. Guard it with `day::lifecycle_supported(..)` or a
`day::require_lifecycle!(..)` compile-time check.

There are two ways to guard, depending on how strict you want to be.

Soft: register only where supported. day::lifecycle::supported is a const fn that knows the backend compiled into this binary, so it’s false on desktop and true on mobile for the mobile-only phases:

if day::lifecycle::supported(Lifecycle::DidEnterBackground) {
    day::on_lifecycle(Lifecycle::DidEnterBackground, || flush_to_disk());
}

(There is also a runtime day::lifecycle_supported(phase) for checks after the app is up.)

Hard: require it at compile time. If your app depends on a phase, assert it and get a build error on a backend that can’t deliver it:

day::require_lifecycle!(Lifecycle::DidEnterBackground);  // compile error on a desktop backend

Quit

An orderly quit can deliver WillTerminate: the Quit menu command (menu_role(MenuRole::Quit)), the platform quit shortcut (⌘Q on macOS, Ctrl+Q elsewhere), and native shutdown callbacks. A forced kill or mobile process reclamation may deliver none. The Quit command exits the app. On GTK a standard app.quit action is registered so ⌘Q / Ctrl+Q and the menu item both work; on macOS the App-menu Quit is standard; Qt/XAML quit their event loops. Use WillTerminate for synchronous cleanup, not as the sole durability point for user data.

Mobile note: iOS/Android apps don’t have a user-facing “quit”. There, WillTerminate means the OS is tearing the app down (Android onDestroy while finishing; iOS applicationWillTerminate:). Prefer DidEnterBackground for “the user left” on mobile.

How it works

on_lifecycle records the closure in a day-core registry keyed by phase. Backends emit Event::Lifecycle(phase) from their native delegate (the same event rail as menu actions); the event pump routes it to dispatch_lifecycle, which runs the phase’s handlers in a reactive batch. Adding lifecycle support to a new backend is two things: implement Toolkit::supports_lifecycle (and the matching const fn lifecycle_supported), and emit Event::Lifecycle(..) at the right native moments.

Launch memory profiling

day launch enables DAY_MEMORY_PROFILE=1 for native apps by default. A process-global sampler reads current-process memory every second, keeps constant-space min/max/sum/count, and appends min/max/average/end MB to the normal exit line. --memory-profile=false disables it. The initial and final samples are included; the average is an arithmetic sample mean and the maximum is a sampled maximum. The sampler uses no UI state or TLS. Abrupt process termination cannot emit a lifecycle exit line. Scripted launches additionally checkpoint host reports and summarize the last observations when the connection is lost; see the DayScript contract in DESIGN.md.

Verification boundary

The snippets are integration fragments; the log lines are illustrative output, not measured results. Review core lifecycle dispatch and task shutdown for ordering. The matrix records intended callback mappings; an OS can kill the process without delivering a final callback. Save important state as it changes rather than waiting for shutdown.