maui-dependency-injection について
.NET MAUI 開発者向けに、依存性注入のライフタイムと登録方法を解説するスキルです。ページや ViewModel の登録、XAML 初期化のタイミング、プラットフォーム固有サービスの注意点を扱います。
主な機能
- 01状態と用途に応じたライフタイム選択
- 02ページと ViewModel の Transient 登録
- 03CreateWindow までサービス解決を延期
- 04サービスロケーターの多用を回避
- 05プラットフォーム別登録の網羅性確認
活用シーン
- 01ページ遷移に応じた ViewModel のライフタイム設定
- 02ページへの依存性注入漏れの調査
- 03DI 準備前に解析される XAML リソースへの対処
- 04プラットフォーム条件によるサービス欠落の防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-dependency-injectionインストール後、Claude Code で/maui-dependency-injectionを入力して読み込まれたか確認します。
同梱ファイルreferences/dependency-injection-api.md
詳細
.NET MAUI の依存性注入
ライフタイムの判断基準
| 質問 | 回答 → ライフタイム |
|---|---|
| 共有状態を保持する、または生成コストが高いですか? | AddSingleton |
| ステートレス、軽量、またはリクエストごとに必要ですか? | AddTransient |
IServiceScope を自分で管理しますか? | AddScoped |
⚠️ MAUI では
AddScopedを避けてください。ページごとのスコープは組み込みで用意されていません。IServiceScopeを手動で作成せずに使用すると、気付かないうちにシングルトンとして動作し、 混乱やエラーの原因になります。
シングルトンの落とし穴
// ❌ ViewModel registered as Singleton — stale data across navigations
builder.Services.AddSingleton<DetailViewModel>();
// ✅ ViewModels are Transient — fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();
ページと ViewModel は Transient として登録してください。共有状態を保持するサービスは Singleton として登録してください(例:
IDataService、HttpClientファクトリ)。
注意点:XAML リソースの解析と DI のタイミング
XAML リソース(App.xaml のスタイルやコンバーター)は、InitializeComponent() の実行中に解析されます。DI コンテナーが完全に利用可能になる前です。リソースやコンバーターでサービスが必要な場合は、コンストラクターではなく CreateWindow() で解決してください。
// ❌ Resolving services during XAML parse — container may not be ready
public App(IDataService data)
{
InitializeComponent(); // XAML parses here
_data = data; // may fail for types not yet resolved
}
// ✅ Defer service resolution to CreateWindow
public partial class App : Application
{
private readonly IServiceProvider _services;
public App(IServiceProvider services)
{
_services = services;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
// Safe — container is fully built
var mainPage = _services.GetRequiredService<MainPage>();
return new Window(new AppShell());
}
}
注意点:未登録のページでは DI が暗黙にスキップされる
Shell の XAML(<ShellContent ContentTemplate="...">)でページを使用していても、builder.Services に登録されていない場合、MAUI はパラメーターなしのコンストラクターでページをインスタンス化します。依存関係は例外も発生せず、暗黙に null になります。
// ❌ Page not registered — constructor injection silently skipped
// builder.Services.AddTransient<DetailPage>(); // missing!
// ✅ Always register pages that need injection
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
アンチパターン:サービスロケーターの過剰使用
// ❌ Service locator scattered through code — hard to test, hides dependencies
public void DoWork()
{
var service = this.Handler.MauiContext.Services.GetService<IDataService>();
service.Load();
}
// ✅ Constructor injection — explicit, testable
public class MyViewModel(IDataService dataService)
{
public void DoWork() => dataService.Load();
}
明示的な解決(
Handler.MauiContext.Services)は、コンストラクター注入が本当に利用できない場合(カスタムハンドラーやプラットフォームコールバック内など)に限って使用してください。
プラットフォーム固有の登録に関する落とし穴
プラットフォームサービスに #if ディレクティブを使用する場合、インターフェイスが常に登録されていることを確認してください。そうしないと、対象外のプラットフォーム上のコンシューマーが実行時に null を受け取ります。
// ❌ No registration on Windows — GetService returns null
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#endif
// ✅ Cover all platforms or provide a no-op fallback
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#endif
チェックリスト
- 注入が必要なすべてのページと ViewModel が
MauiProgram.csに登録されている - ページ/ViewModel は
AddTransient、共有サービスはAddSingletonで登録されている - 可能な限りすべての箇所でコンストラクター注入を使用し、サービスロケーターは最後の手段としてのみ使用している
- テストでモックする必要があるサービスにはインターフェイスが定義されている
- プラットフォーム固有の
#if登録がすべての対象プラットフォームをカバーしている(またはフォールバックを提供している) - 遅延バインドされるサービスは、XAML の解析中ではなく
CreateWindow()で解決されている -
AddScopedはIServiceScopeを手動で管理する場合にのみ使用している