Skills maui-app-lifecycle
git clone https://github.com/dotnet/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dotnet/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/dotnet-maui/skills/maui-app-lifecycle" ~/.claude/skills/dotnet-skills-maui-app-lifecycle && rm -rf "$T"
plugins/dotnet-maui/skills/maui-app-lifecycle/SKILL.md.NET MAUI App Lifecycle
Handle application state transitions correctly in .NET MAUI. This skill covers the cross-platform Window lifecycle events, their platform-native mappings, and patterns for preserving state across backgrounding and resume cycles.
When to Use
- Saving or restoring state when the app backgrounds or resumes
- Subscribing to Window lifecycle events (Created, Activated, Deactivated, Stopped, Resumed, Destroying)
- Hooking into platform-native lifecycle callbacks via
ConfigureLifecycleEvents - Deciding where to place initialization, teardown, or refresh logic
- Understanding the difference between Deactivated and Stopped
When Not to Use
- Page-level navigation events — use Shell navigation guidance instead
- Registering services at startup — use dependency injection guidance instead
- Calling platform-specific APIs outside lifecycle context — use platform invoke guidance instead
Inputs
- The target lifecycle transition (e.g., "save draft when backgrounded", "refresh data on resume")
- Which platforms the developer targets (Android, iOS, Mac Catalyst, Windows)
- Whether the app uses multiple windows (iPad, Mac Catalyst, desktop Windows)
App States
A .NET MAUI app moves through four states:
| State | Description |
|---|---|
| Not Running | Process does not exist |
| Running | Foreground, receiving input |
| Deactivated | Visible but lost focus (dialog, split-screen, notification shade) |
| Stopped | Fully backgrounded, UI not visible |
Typical flow: Not Running → Running → Deactivated → Stopped → Running (resumed) or Not Running (terminated).
Window Lifecycle Events
Microsoft.Maui.Controls.Window exposes six cross-platform events:
| Event | Fires when |
|---|---|
| Native window allocated |
| Window receives input focus |
| Window loses focus (may still be visible) |
| Window is no longer visible |
| Window returns to foreground after Stopped |
| Native window is being torn down |
Subscribing via CreateWindow
Override
CreateWindow in your App class and attach event handlers:
public partial class App : Application { protected override Window CreateWindow(IActivationState? activationState) { var window = base.CreateWindow(activationState); window.Created += (s, e) => Debug.WriteLine("Created"); window.Activated += (s, e) => Debug.WriteLine("Activated"); window.Deactivated += (s, e) => Debug.WriteLine("Deactivated"); window.Stopped += (s, e) => Debug.WriteLine("Stopped"); window.Resumed += (s, e) => Debug.WriteLine("Resumed"); window.Destroying += (s, e) => Debug.WriteLine("Destroying"); return window; } }
Subscribing via a Custom Window Subclass
Create a
Window subclass and override the virtual methods:
public class AppWindow : Window { public AppWindow(Page page) : base(page) { } protected override void OnActivated() { /* refresh UI */ } protected override void OnStopped() { /* save state */ } protected override void OnResumed() { /* restore state */ } protected override void OnDestroying() { /* cleanup */ } }
Return it from
CreateWindow:
protected override Window CreateWindow(IActivationState? activationState) => new AppWindow(new AppShell());
Workflow: Save and Restore State on Background
- Identify transient state — draft text, scroll position, form inputs, timer values.
- Save in
— useOnStopped
for small values or file serialization for larger state.Preferences - Restore in
— read back saved values and apply to your view model.OnResumed - Also save in
on Android — the back button can skipOnDestroying
entirely.Stopped - Keep handlers fast — complete within 1–2 seconds to avoid ANR on Android or watchdog kills on iOS.
protected override void OnStopped() { base.OnStopped(); Preferences.Set("draft_text", _viewModel.DraftText); Preferences.Set("scroll_y", _viewModel.ScrollY); } protected override void OnResumed() { base.OnResumed(); _viewModel.DraftText = Preferences.Get("draft_text", string.Empty); _viewModel.ScrollY = Preferences.Get("scroll_y", 0.0); } protected override void OnDestroying() { base.OnDestroying(); // Android back-button can skip Stopped Preferences.Set("draft_text", _viewModel.DraftText); }
Platform Lifecycle Mapping
Android
| Window Event | Android Callback |
|---|---|
| Created | |
| Activated | |
| Deactivated | |
| Stopped | |
| Resumed | → → |
| Destroying | |
iOS / Mac Catalyst
| Window Event | UIKit Callback |
|---|---|
| Created | / |
| Activated | |
| Deactivated | |
| Stopped | |
| Resumed | |
| Destroying | |
Windows (WinUI)
| Window Event | WinUI Callback |
|---|---|
| Created | |
| Activated | (foreground) |
| Deactivated | (background) |
| Stopped | (false) |
| Resumed | (true) |
| Destroying | |
Hooking Native Lifecycle Directly
Use
ConfigureLifecycleEvents in MauiProgram.cs when you need platform-specific callbacks beyond what Window events provide:
builder.ConfigureLifecycleEvents(events => { #if ANDROID events.AddAndroid(android => android .OnCreate((activity, bundle) => Debug.WriteLine("Android OnCreate")) .OnResume(activity => Debug.WriteLine("Android OnResume")) .OnPause(activity => Debug.WriteLine("Android OnPause")) .OnStop(activity => Debug.WriteLine("Android OnStop")) .OnDestroy(activity => Debug.WriteLine("Android OnDestroy"))); #elif IOS || MACCATALYST events.AddiOS(ios => ios .DidBecomeActive(app => Debug.WriteLine("iOS DidBecomeActive")) .WillResignActive(app => Debug.WriteLine("iOS WillResignActive")) .DidEnterBackground(app => Debug.WriteLine("iOS DidEnterBackground")) .WillEnterForeground(app => Debug.WriteLine("iOS WillEnterForeground"))); #elif WINDOWS events.AddWindows(windows => windows .OnLaunched((app, args) => Debug.WriteLine("Windows OnLaunched")) .OnActivated((window, args) => Debug.WriteLine("Windows Activated")) .OnClosed((window, args) => Debug.WriteLine("Windows Closed"))); #endif });
Common Pitfalls
-
Resumed does not fire on first launch. The initial sequence is
→Created
. UseActivated
for logic that must run on every foreground entry, notOnActivated
.OnResumed -
Deactivated ≠ Stopped. A dialog, split-screen, or notification pull-down triggers
withoutDeactivated
. Do not perform heavy saves inStopped
— the app may never actually background.OnDeactivated -
Android back button skips Stopped. On Android, pressing back may call
directly withoutDestroying
. Place critical save logic in bothStopped
andOnStopped
.OnDestroying -
Multi-window apps fire events independently. On iPad, Mac Catalyst, and desktop Windows each
instance fires its own lifecycle events. Do not assume a single global lifecycle.Window -
Long-running handlers cause kills. Android enforces a ~5 second ANR timeout; iOS has limited background execution time. Keep lifecycle handlers synchronous and fast — use
for quick saves, not database writes.Preferences -
Do not use legacy Xamarin.Forms lifecycle methods.
,Application.OnStart()
, andApplication.OnSleep()
exist for backward compatibility but bypass Window-level events. In .NET MAUI, preferApplication.OnResume()
lifecycle events (Window
,OnActivated
,OnStopped
, etc.) for correct multi-window behavior.OnResumed