Platform Lifecycle Events in .NET MAUI
Mobile apps don't behave like desktop applications. The OS can suspend, resume, or terminate your app at any moment. Understanding lifecycle events is essential for saving state, managing resources, and keeping your users' data safe.
The Cross-Platform Lifecycle
MAUI provides a unified lifecycle model that maps to the underlying platform events. Your App class can override these methods:
public partial class App : Application
{
protected override Window CreateWindow(IActivationState activationState)
{
var window = new Window(new AppShell());
window.Created += (s, e) =>
Debug.WriteLine("Window created");
window.Activated += (s, e) =>
Debug.WriteLine("App is now in the foreground");
window.Deactivated += (s, e) =>
Debug.WriteLine("App moved to the background");
window.Stopped += (s, e) =>
Debug.WriteLine("App is no longer visible");
window.Resumed += (s, e) =>
Debug.WriteLine("App resumed from background");
window.Destroying += (s, e) =>
Debug.WriteLine("App is being destroyed");
return window;
}
}
These events map to different native events on each platform:
| MAUI Event | Android | iOS |
|---|---|---|
| Created | OnCreate |
FinishedLaunching |
| Activated | OnResume |
OnActivated |
| Deactivated | OnPause |
OnResignActivation |
| Stopped | OnStop |
DidEnterBackground |
| Resumed | OnRestart |
WillEnterForeground |
| Destroying | OnDestroy |
WillTerminate |
Saving State on Background
The most critical lifecycle task: save user state before the app is suspended. Android is particularly aggressive about killing background apps to reclaim memory.
public partial class App : Application
{
private readonly IStateService _stateService;
public App(IStateService stateService)
{
_stateService = stateService;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState activationState)
{
var window = new Window(new AppShell());
window.Deactivated += async (s, e) =>
{
await _stateService.SaveStateAsync();
};
window.Resumed += async (s, e) =>
{
await _stateService.RestoreStateAsync();
};
return window;
}
}
A simple state service using Preferences:
public class StateService : IStateService
{
public Task SaveStateAsync()
{
Preferences.Set("last_page", Shell.Current.CurrentState.Location.ToString());
Preferences.Set("last_active", DateTime.UtcNow.ToString("O"));
return Task.CompletedTask;
}
public Task RestoreStateAsync()
{
var lastActive = Preferences.Get("last_active", string.Empty);
if (DateTime.TryParse(lastActive, out var date)
&& DateTime.UtcNow - date < TimeSpan.FromHours(1))
{
var lastPage = Preferences.Get("last_page", string.Empty);
if (!string.IsNullOrEmpty(lastPage))
{
Shell.Current.GoToAsync(lastPage);
}
}
return Task.CompletedTask;
}
}
Platform-Specific Lifecycle Events
When the cross-platform events aren't enough, hook into platform-specific lifecycle events in MauiProgram.cs:
builder.ConfigureLifecycleEvents(events =>
{
#if ANDROID
events.AddAndroid(android => android
.OnBackPressed((activity) =>
{
Debug.WriteLine("Android back button pressed");
return false; // return true to prevent default behaviour
})
.OnNewIntent((activity, intent) =>
{
// Handle deep links or notifications
ProcessIntent(intent);
})
.OnRequestPermissionsResult((activity, requestCode, permissions, results) =>
{
// Handle permission responses
}));
#elif IOS
events.AddiOS(ios => ios
.OpenUrl((app, url, options) =>
{
// Handle URL schemes
Debug.WriteLine($"Opened via URL: {url}");
return true;
})
.PerformActionForShortcutItem((app, item, handler) =>
{
// Handle 3D Touch / quick actions
}));
#endif
});
Page Lifecycle
Individual pages have their own lifecycle events:
public partial class DashboardPage : ContentPage
{
private readonly DashboardViewModel _viewModel;
private CancellationTokenSource _refreshCts;
public DashboardPage(DashboardViewModel viewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
}
protected override void OnAppearing()
{
base.OnAppearing();
_refreshCts = new CancellationTokenSource();
_ = _viewModel.StartAutoRefreshAsync(_refreshCts.Token);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_refreshCts?.Cancel();
_refreshCts?.Dispose();
}
}
OnAppearing fires every time the page becomes visible — including when navigating back to it. Use it to refresh data. OnDisappearing fires when navigating away — use it to cancel timers and background work.
Managing Connectivity Changes
Combine lifecycle events with connectivity monitoring:
public partial class App : Application
{
protected override Window CreateWindow(IActivationState activationState)
{
var window = new Window(new AppShell());
Connectivity.ConnectivityChanged += OnConnectivityChanged;
window.Destroying += (s, e) =>
{
Connectivity.ConnectivityChanged -= OnConnectivityChanged;
};
return window;
}
private void OnConnectivityChanged(object sender,
ConnectivityChangedEventArgs e)
{
if (e.NetworkAccess == NetworkAccess.Internet)
{
// Queue sync of offline changes
WeakReferenceMessenger.Default.Send(new ConnectivityRestoredMessage());
}
}
}
Handling Low Memory
On mobile, the OS can send low memory warnings before killing your app:
builder.ConfigureLifecycleEvents(events =>
{
#if ANDROID
events.AddAndroid(android => android
.OnTrimMemory((activity, level) =>
{
if (level == Android.Content.TrimMemory.RunningLow)
{
// Clear caches, release images
ImageCache.Instance.Clear();
}
}));
#elif IOS
events.AddiOS(ios => ios
.DidReceiveMemoryWarning((app) =>
{
ImageCache.Instance.Clear();
GC.Collect();
}));
#endif
});
Best Practices
Save early, save often. Don't wait for the Stopped event — Deactivated is your last reliable chance on some platforms. Android may skip Stopped and go straight to killing your process.
Don't do heavy work in lifecycle handlers. The OS gives you limited time in background transitions. If you need to finish a long operation, request background time on iOS or use a foreground service on Android.
Unsubscribe from events. Always unsubscribe from platform events in Destroying. Leaked event handlers prevent garbage collection and can cause crashes if they fire after your app's objects have been collected.
Test lifecycle scenarios explicitly. Use the Android emulator's "Don't keep activities" developer option to aggressively destroy activities. On iOS, simulate memory warnings in the simulator via Debug > Simulate Memory Warning.
Lifecycle management isn't glamorous, but it's the difference between an app that loses user data and one that handles interruptions gracefully. Get it right from the start rather than bolting it on later.