Start Debugging

.NET MAUI の Android アプリを API レベル 36 ターゲットに移行する

Google Play は 2026-08-31 からターゲット API レベル 36 を必須にし、延長は 2026-11-01 までです。net9.0-android から API 36 までの .NET MAUI の移行手順をすべて解説します。target framework の変更、古いレベルに静かに固定してしまう uses-sdk、オプトアウトできなくなった edge-to-edge、予測型の戻る操作、そして大画面のルールまで。

ビルド側の変更は 1 行です。移行の本体は動作の変更のほうです。Google Play は 2026-08-31 から、新規アプリとアプリ更新にターゲット API レベル 36 を要求しはじめました。アプリ単位の延長は Play Console から 2026-11-01 まで申請できます。今週アップデートがリジェクトされたなら、理由はこれです。.NET MAUI アプリでは、ターゲット API レベルは自分で編集するマニフェストの設定ではありません。TargetFramework に含まれる Android プラットフォームバージョンから導出され、.NET 9 は API 35 までしか到達しません。つまりこれはマニフェストの微調整ではなく、.NET SDK を .NET 10 (または .NET 11) へ上げる作業です。小さなアプリなら 1 日、画面の向きを固定しているもの、独自の戻るボタンを持つもの、インセットを手作業で調整しているものなら 1 スプリントを見込んでください。本記事は到達点として .NET 10 と .NET MAUI 10.0.100 (2026-08-20 リリース) を対象にし、.NET 11 との差分も併記します。

Play が確認しているのがターゲットレベルである理由

何が壊れるか

領域ターゲット API 36 での変更深刻度
Edge-to-edgewindowOptOutEdgeToEdgeEnforcement は非推奨になり、Android 16 端末では無視されます
.NET MAUI のセーフエリア.NET 10 以降 ContentPage.SafeAreaEdges の既定値が None になり、ページが画面端まで広がります
予測型の戻る操作ホームへの復帰やアクティビティ間のアニメーションが既定で有効になり、OnBackPressed は呼ばれません
大画面sw600dp 以上で android:screenOrientationresizableActivityminAspectRatiomaxAspectRatio が無視されます高 (タブレット、折りたたみ端末)
.NET SDKAPI 36 には net10.0-android 以降が必要で、.NET 9 のワークロードは API 35 で止まります
最小 API.NET 11 は下限を API 21 から API 24 に引き上げます中 (.NET 11 のみ)
テキスト描画android:elegantTextHeight は非推奨になり無視されます
スケジューリングScheduledExecutorService.scheduleAtFixedRate は取りこぼした実行を最大 1 回しか補いません
ヘルスセンサーBODY_SENSORS は粒度の細かい android.permissions.health 権限に置き換わります低 (心拍を読む場合を除く)

上の 2 行は重なり合います。API 36 を得るために .NET 10 へ上げると、同じコミットで .NET MAUI 自身のセーフエリアの既定値も変わります。そのため、.NET 9 でターゲット 35 のときは問題なく見えていたアプリが、独立した 2 つの理由でタイトルバーがステータスバーの下に潜った状態で出てくることがあります。

事前チェックリスト

移行手順

  1. 今のターゲットが実際にいくつなのかを確認します。 csproj ではなく、ビルドが生成するマージ済みマニフェストを読んでください。

    dotnet build -f net9.0-android -c Release
    grep -o 'targetSdkVersion="[0-9.]*"' obj/Release/net9.0-android/AndroidManifest.xml

    確認: 数値が 1 つ出力されます。それが TargetFramework の Android プラットフォームバージョンより低ければ、何かが値を固定しています。その場合は手順 3 が最重要です。

  2. target framework を .NET 10 に移します。 TFM の Android プラットフォームバージョンがそのまま targetSdkVersion になるため、この 1 箇所の編集が移行の実体です。

    <!-- .csproj, .NET 10, .NET MAUI 10.0.100 -->
    <PropertyGroup>
      <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
      <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
    </PropertyGroup>

    素の net10.0-android は API 36 に解決され、これが .NET 10 のドキュメント化された既定値です。後で .NET 11 に移ったときに勝手に動くのではなくビルドを失敗させたいなら、net10.0-android36.0 と明示的に固定してください。.NET for Android は .NET 11 Preview 5 で API 37 を安定版に昇格させ、.NET 11 プロジェクトの既定を net11.0-android37 にしているためです。$(SupportedOSPlatformVersion) は別の軸で、minSdkVersion になるものであり、Play の要件とは関係ありません。

    確認: 再ビルドし、手順 1 の grepobj/Release/net10.0-android/AndroidManifest.xml に対して実行します。targetSdkVersion="36" と表示される必要があります。

  3. マニフェストに直書きされた uses-sdk を削除します。 手順 2 が何も効いていないように見える最大の原因がこれです。.NET for Android がテンプレートのマニフェストに targetSdkVersion を書き込むのは、そこにまだ値が無い場合だけで、明示的な値が無条件に優先されます (ManifestDocument.cs)。

    <!-- Platforms/Android/AndroidManifest.xml: delete the uses-sdk line entirely -->
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
      <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
      <application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true" />
    </manifest>

    Microsoft 自身の XA5207 のガイダンスが、SDK 更新をまたいでターゲットレベルを保つためにまさにこの要素を追加するよう案内していたので、Xamarin.Forms 時代のプロジェクトには今も残っています。現在の .NET MAUI テンプレートには uses-sdk 要素が一切なく、それが望ましい状態です。

    確認: grep -c uses-sdk Platforms/Android/AndroidManifest.xml0 を返し、マージ済みマニフェストには引き続き targetSdkVersion="36" が出ること。

  4. edge-to-edge の方針を決めます。もう選択の余地はありません。 ターゲット 36 では、windowOptOutEdgeToEdgeEnforcement 属性は Android 16 端末で非推奨かつ無効です。Platforms/Android/Resources/values/styles.xml に書いていたなら削除してください。そのうえで、.NET 10 の既定値である None をそのまま受け入れるのではなく、ページごとに SafeAreaEdges の値を選びます。

    <!-- .NET MAUI 10.0.100: ContentPage defaults to SafeAreaEdges="None" -->
    <ContentPage SafeAreaEdges="Container">
        <Grid SafeAreaEdges="Container" RowDefinitions="Auto,*">
            <Label Text="Not under the status bar" />
        </Grid>
    </ContentPage>

    Container は、システムバーやディスプレイのカットアウトを避ける .NET 9 の動作を再現します。All はさらにキーボードも避けるので、Android の platform-specific である WindowSoftInputModeAdjust.Resize に依存していたならこちらです。None は没入型の選択肢であり、意図して選ぶものであって、うっかり引き継ぐ既定値ではありません。

    確認: Android 16 端末で、主要な 3 画面において、ライトテーマとダークテーマの両方でステータスバーとジェスチャーナビゲーションバーがタップ可能なコントロールに重ならないこと。

  5. 予測型の戻る操作に飲み込まれる前に、独自の戻る処理を直します。 ターゲット 36 では予測型の戻るアニメーションが既定で有効になり、onBackPressed() は呼ばれず、KeyEvent.KEYCODE_BACK もディスパッチされません。次のようなアクティビティのオーバーライドは動かなくなります。

    // Broken at targetSdkVersion 36 on Android 16
    public override void OnBackPressed()
    {
        if (_hasUnsavedChanges) { ShowConfirmDialog(); return; }
        base.OnBackPressed();
    }

    代わりに .NET MAUI 自身のナビゲーション層で処理してください。こちらはプラットフォームをまたいで動き続けます。

    // .NET MAUI 10.0.100, cross-platform
    protected override bool OnBackButtonPressed()
    {
        if (!_hasUnsavedChanges)
            return base.OnBackButtonPressed();
    
        Dispatcher.Dispatch(async () => await DisplayAlertAsync("Discard changes?", "...", "OK"));
        return true; // handled
    }

    Android 側の逃げ道は <application> または個別の <activity> に付ける android:enableOnBackInvokedCallback="false" ですが、これは応急処置であって解決策ではありません。

    確認: 画面の端からスワイプして指を止めます。プレビューのアニメーションが見え、指を離したときにハンドラーの意図どおりの動作になること。

  6. 固定した画面の向きとアスペクト比を洗い出します。 sw600dp 以上のディスプレイでは、ターゲット 36 によって Android が android:screenOrientationandroid:resizableActivityandroid:minAspectRatioandroid:maxAspectRatio と、実行時の SetRequestedOrientation を無視します。.NET MAUI では通常、MainActivity の属性が該当します。

    // Ignored on sw600dp+ displays at targetSdkVersion 36
    [Activity(ScreenOrientation = ScreenOrientation.Portrait, /* ... */)]
    public class MainActivity : MauiAppCompatActivity { }

    一時的なオプトアウトはマニフェストのプロパティで、Google は API レベル 37 では適用されなくなると明言しています。

    <application>
      <property android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
                android:value="true" />
    </application>

    確認: タブレットまたは折りたたみ端末のエミュレーターで実行して回転させます。横向きでレイアウトが使い物にならないならレイアウトを直してください。オプトアウトで買える時間は 1 年だけです。

  7. 持っていないプラットフォームに対してビルドしないよう CI を更新します。 エージェントに API 36 が無いと XA5207 として表面化しますが、対処はポータルからのダウンロードではなくビルドターゲットです。

    dotnet build -t:InstallAndroidDependencies -f net10.0-android \
      -p:AndroidSdkDirectory="$ANDROID_HOME" \
      -p:AcceptAndroidSDKLicenses=true

    -f 引数は必須で、付けないと MSBuild が MSB4057: The target "InstallAndroidDependencies" does not exist in the project を報告します。

    確認: SDK キャッシュが空の状態からのクリーンな CI 実行で、XA5207 なしに署名済み AAB が生成されること。

検証チェックリスト

ロールバック計画

TargetFrameworknet9.0-android に戻せば、以前のターゲットレベルと以前の .NET MAUI のセーフエリアの動作が復元されます。.NET 10 の API を新たに使い始めていない限り、これはきれいなリバートです。戻せないのは Play 側です。ターゲット 36 の AAB を一度公開すると、その後は同じトラックにそれより低いターゲットレベルを公開できません。Play はアップロードのたびに下限を強制するからです。内部トラックをロールバックの猶予期間とみなし、production への昇格は片道と考えてください。

実際に時間を奪う落とし穴

関連記事

参照元

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る