maui-app-lifecycle について
.NET MAUI App Lifecycle は、.NET MAUI 開発者向けにウィンドウ状態の変化とクロスプラットフォームのライフサイクルイベントを解説するスキルです。ネイティブコールバックとの対応や、バックグラウンド移行・復帰時の状態保存方法を扱います。
主な機能
- 016 種類のウィンドウライフサイクルイベントの解説
- 02Android、iOS、Mac Catalyst、Windows のコールバック対応
- 03バックグラウンド時の状態保存と復元
- 04イベント登録とメソッドオーバーライドの例
- 05ConfigureLifecycleEvents の使い方
- 06マルチウィンドウ動作と一般的な注意点
活用シーン
- 01アプリのバックグラウンド移行時に下書きやフォームを保存
- 02復帰時に UI を更新、または状態を復元
- 03プラットフォーム固有のライフサイクルコールバックを利用
- 04フォーカス喪失とバックグラウンド移行を区別
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill maui-app-lifecycleインストール後、Claude Code で/maui-app-lifecycleを入力して読み込まれたか確認します。
同梱ファイルreferences/lifecycle-api.md
詳細
.NET MAUI アプリのライフサイクル
.NET MAUI でアプリの状態遷移を正しく処理します。このスキルでは、クロスプラットフォームの Window ライフサイクルイベントとプラットフォームネイティブの対応関係、およびバックグラウンド移行と再開のサイクルを通じて状態を保持するパターンについて説明します。
使用する場面
- アプリがバックグラウンドに移行または再開するときに状態を保存、復元する
- Window ライフサイクルイベント(Created、Activated、Deactivated、Stopped、Resumed、Destroying)をサブスクライブする
ConfigureLifecycleEventsを介してプラットフォームネイティブのライフサイクルコールバックにフックする- 初期化、後処理、更新のロジックを配置する場所を判断する
- Deactivated と Stopped の違いを理解する
使用しない場面
- ページレベルのナビゲーションイベント — 代わりに Shell ナビゲーションのガイダンスを使用します
- 起動時のサービス登録 — 代わりに依存関係注入のガイダンスを使用します
- ライフサイクルのコンテキスト外でプラットフォーム固有の API を呼び出す — 代わりにプラットフォーム呼び出しのガイダンスを使用します
入力
- 対象となるライフサイクル遷移(例:「バックグラウンド移行時に下書きを保存する」「再開時にデータを更新する」)
- 開発者が対象とするプラットフォーム(Android、iOS、Mac Catalyst、Windows)
- アプリが複数ウィンドウを使用するかどうか(iPad、Mac Catalyst、デスクトップ Windows)
アプリの状態
.NET MAUI アプリは 4 つの状態を遷移します。
| 状態 | 説明 |
|---|---|
| 未実行 | プロセスが存在しません |
| 実行中 | フォアグラウンドで入力を受け付けています |
| 非アクティブ | 表示されていますが、フォーカスを失っています(ダイアログ、画面分割、通知シェード) |
| 停止 | 完全にバックグラウンドに移行し、UI は表示されていません |
一般的な流れ:未実行 → 実行中 → 非アクティブ → 停止 → 実行中(再開)または未実行(終了)。
Window ライフサイクルイベント
Microsoft.Maui.Controls.Window は 6 つのクロスプラットフォームイベントを公開します。
| イベント | 発生するタイミング |
|---|---|
Created | ネイティブウィンドウが割り当てられたとき |
Activated | ウィンドウが入力フォーカスを受け取ったとき |
Deactivated | ウィンドウがフォーカスを失ったとき(表示されたままの場合があります) |
Stopped | ウィンドウが表示されなくなったとき |
Resumed | Stopped の後、ウィンドウがフォアグラウンドに戻ったとき |
Destroying | ネイティブウィンドウが破棄されるとき |
CreateWindow を介したサブスクライブ
App クラスで CreateWindow をオーバーライドし、イベントハンドラーを追加します。
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;
}
}
カスタム Window サブクラスを介したサブスクライブ
Window のサブクラスを作成し、仮想メソッドをオーバーライドします。
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 */ }
}
CreateWindow から返します。
protected override Window CreateWindow(IActivationState? activationState)
=> new AppWindow(new AppShell());
ワークフロー:バックグラウンド移行時に状態を保存し、復元する
- 一時的な状態を特定する — 下書きテキスト、スクロール位置、フォーム入力、タイマーの値などです。
OnStoppedで保存する — 小さな値にはPreferencesを、大きな状態にはファイルへのシリアル化を使用します。OnResumedで復元する — 保存した値を読み込み、ビューモデルに適用します。- Android では
OnDestroyingでも保存する — 戻るボタンを押すと、Stoppedが発生しない場合があります。 - ハンドラーの処理を短く保つ — Android での ANR や iOS でのウォッチドッグによる強制終了を避けるため、1~2 秒以内に完了させます。
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);
}
プラットフォームのライフサイクル対応関係
Android
| Window イベント | Android コールバック |
|---|---|
| Created | OnCreate |
| Activated | OnResume |
| Deactivated | OnPause |
| Stopped | OnStop |
| Resumed | OnRestart → OnStart → OnResume |
| Destroying | OnDestroy |
iOS / Mac Catalyst
| Window イベント | UIKit コールバック | AddiOS ビルダーメソッド |
|---|---|---|
| Created | WillFinishLaunching / SceneWillConnect | .WillFinishLaunching() / .SceneWillConnect() |
| Activated | DidBecomeActive | .OnActivated() |
| Deactivated | WillResignActive | .OnResignActivation() |
| Stopped | DidEnterBackground | .DidEnterBackground() |
| Resumed | WillEnterForeground | .WillEnterForeground() |
| Destroying | WillTerminate | .WillTerminate() |
⚠️ UIKit のセレクター名と
AddiOSビルダーメソッド名は、アクティブ化に関して異なります。.DidBecomeActive()または.WillResignActive()というビルダーメソッドはありません。.OnActivated()と.OnResignActivation()を使用してください。そうしないと、コードはコンパイルできません。
Windows(WinUI)
| ウィンドウイベント | WinUI コールバック |
|---|---|
| 作成 | OnLaunched |
| アクティブ化 | Activated(フォアグラウンド) |
| 非アクティブ化 | Activated(バックグラウンド) |
| 停止 | VisibilityChanged(false) |
| 再開 | VisibilityChanged(true) |
| 破棄 | Closed |
ネイティブのライフサイクルを直接フックする
Window イベントで提供されるもの以外にプラットフォーム固有のコールバックが必要な場合は、MauiProgram.cs で ConfigureLifecycleEvents を使用します。
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
.OnActivated(app => Debug.WriteLine("iOS OnActivated"))
.OnResignActivation(app => Debug.WriteLine("iOS OnResignActivation"))
.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
});
よくある落とし穴
-
初回起動時に Resumed は呼び出されません。 初回のシーケンスは
Created→Activatedです。フォアグラウンドに入るたびに実行する必要があるロジックには、OnResumedではなくOnActivatedを使用します。 -
Deactivated と Stopped は異なります。 ダイアログ、画面分割、通知パネルの引き下げでは、
Stoppedを伴わずにDeactivatedが発生します。アプリが実際にバックグラウンドに移行するとは限らないため、OnDeactivatedで負荷の高い保存処理を行わないでください。 -
Android の戻るボタンでは Stopped がスキップされます。 Android では、戻るボタンを押すと
Stoppedを経ずにDestroyingが直接呼び出されることがあります。重要な保存ロジックはOnStoppedとOnDestroyingの両方に配置してください。 -
マルチウィンドウアプリではイベントが個別に発生します。 iPad、Mac Catalyst、デスクトップ Windows では、それぞれの
Windowインスタンスが独自のライフサイクルイベントを発生させます。グローバルなライフサイクルが 1 つだけだと想定しないでください。 -
処理時間の長いハンドラーはアプリの強制終了につながります。 Android では約 5 秒の ANR タイムアウトが適用され、iOS ではバックグラウンド実行時間が制限されています。ライフサイクルハンドラーは同期的かつ高速に保ち、すばやく保存するにはデータベースへの書き込みではなく
Preferencesを使用します。 -
レガシーな Xamarin.Forms のライフサイクルメソッドは使用しないでください。
Application.OnStart()、Application.OnSleep()、Application.OnResume()は後方互換性のために存在しますが、Window レベルのイベントを迂回します。.NET MAUI では、マルチウィンドウで正しく動作させるために、Windowのライフサイクルイベント(OnActivated、OnStopped、OnResumedなど)を優先してください。