Start Debugging

Migrate a .NET MAUI Android app to target Android API level 36

Google Play required target API 36 from 2026-08-31, with extensions running to 2026-11-01. Here is the full .NET MAUI path from net9.0-android to API 36: the target framework bump, the hardcoded uses-sdk that silently pins you to the old level, edge-to-edge with no opt-out, predictive back, and the large-screen orientation rules.

The build change is one line. The behaviour changes are the migration. Google Play started requiring target API level 36 for new apps and app updates on 2026-08-31, with a per-app extension available through Play Console until 2026-11-01, so if your update was rejected this week this is why. On a .NET MAUI app the target API level is not a manifest setting you edit, it is derived from the Android platform version in your TargetFramework, and .NET 9 tops out at API 35. That means this is a .NET SDK upgrade to .NET 10 (or .NET 11), not a manifest tweak. Budget a day for a small app and a sprint for anything with a locked orientation, a custom back button, or hand-tuned insets. This guide targets .NET 10 with .NET MAUI 10.0.100 (released 2026-08-20) as the landing spot, and notes where .NET 11 differs.

Why the target level, specifically, is what Play checks

What breaks

AreaChange at target API 36Severity
Edge-to-edgewindowOptOutEdgeToEdgeEnforcement is deprecated and ignored on Android 16 deviceshigh
.NET MAUI safe areasContentPage.SafeAreaEdges defaults to None from .NET 10, so pages go edge-to-edgehigh
Predictive backBack-to-home and cross-activity animations are on by default; OnBackPressed is not calledhigh
Large screensandroid:screenOrientation, resizableActivity, minAspectRatio, maxAspectRatio ignored at sw600dp and abovehigh (tablets, foldables)
.NET SDKAPI 36 needs net10.0-android or later; the .NET 9 workload stops at API 35high
Minimum API.NET 11 raises the floor from API 21 to API 24medium (.NET 11 only)
Text renderingandroid:elegantTextHeight is deprecated and ignoredlow
SchedulingScheduledExecutorService.scheduleAtFixedRate replays at most one missed executionlow
Health sensorsBODY_SENSORS replaced by granular android.permissions.health permissionslow (unless you read heart rate)

The first two rows compound. Upgrading to .NET 10 to get API 36 also changes .NET MAUI’s own safe area default in the same commit, so an app that looked fine on .NET 9 at target 35 can come out the other side with a title bar under the status bar for two independent reasons.

Pre-flight checklist

Migration steps

  1. Find out what you actually target today. Do not read the csproj, read the merged manifest that the build produces:

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

    Verify: you get a single number. If it is lower than the Android platform version in your TargetFramework, something is pinning it, and step 3 is the one that matters most for you.

  2. Move the target framework to .NET 10. The Android platform version in the TFM is what becomes targetSdkVersion, so this single edit is the actual migration:

    <!-- .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>

    Bare net10.0-android resolves to API 36, which is the documented .NET 10 default. Pin it explicitly as net10.0-android36.0 if you want the build to fail rather than drift when you later move to .NET 11, because .NET for Android graduated API 37 to stable in .NET 11 Preview 5 and now defaults .NET 11 projects to net11.0-android37. $(SupportedOSPlatformVersion) is a separate axis: it becomes minSdkVersion and has nothing to do with the Play requirement.

    Verify: rebuild and re-run the grep from step 1 against obj/Release/net10.0-android/AndroidManifest.xml. It must print targetSdkVersion="36".

  3. Delete any hardcoded uses-sdk from your manifest. This is the single most common reason step 2 appears to do nothing. .NET for Android only writes targetSdkVersion when the template manifest does not already have one, and an explicit value wins outright (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’s own XA5207 guidance told people to add exactly this element to hold a target level across an SDK upgrade, so plenty of Xamarin.Forms era projects still carry it. The current .NET MAUI template ships no uses-sdk element at all, which is the state you want.

    Verify: grep -c uses-sdk Platforms/Android/AndroidManifest.xml returns 0, and the merged manifest still shows targetSdkVersion="36".

  4. Decide your edge-to-edge story, because you no longer get a vote. At target 36 the windowOptOutEdgeToEdgeEnforcement attribute is deprecated and disabled on Android 16 devices. If you had it in Platforms/Android/Resources/values/styles.xml, delete it. Then pick a SafeAreaEdges value per page rather than accepting the .NET 10 default of None:

    <!-- .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 reproduces the .NET 9 behaviour of staying clear of system bars and cutouts. All also avoids the keyboard, which is what you want if you relied on the Android WindowSoftInputModeAdjust.Resize platform-specific. None is the immersive option, and it is a deliberate choice, not a default you should inherit by accident.

    Verify: on an Android 16 device, the status bar and gesture navigation bar do not overlap any tappable control on your top three screens, in both light and dark themes.

  5. Fix custom back handling before predictive back eats it. At target 36 the predictive back animations are enabled by default, onBackPressed() is not called, and KeyEvent.KEYCODE_BACK is not dispatched. Any activity override like this stops running:

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

    Handle it in .NET MAUI’s own navigation surface instead, which keeps working across platforms:

    // .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
    }

    The Android escape hatch is android:enableOnBackInvokedCallback="false" on <application> or a single <activity>, and it is a stopgap, not a fix.

    Verify: swipe from the screen edge and hold. You should see the predictive peek animation, and releasing should do what your handler intends.

  6. Audit locked orientation and fixed aspect ratios. On displays at sw600dp and above, target 36 makes Android ignore android:screenOrientation, android:resizableActivity, android:minAspectRatio and android:maxAspectRatio, along with SetRequestedOrientation at runtime. In .NET MAUI that usually means an attribute on MainActivity:

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

    The temporary opt-out is a manifest property, and Google has stated it stops applying at API level 37:

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

    Verify: run on a tablet or foldable emulator and rotate. If the layout is unusable in landscape, fix the layout, because the opt-out buys you one year.

  7. Update CI so it does not build against a platform it does not have. Missing API 36 on an agent surfaces as XA5207, and the fix is a target, not a portal download:

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

    The -f argument is mandatory, otherwise MSBuild reports MSB4057: The target "InstallAndroidDependencies" does not exist in the project.

    Verify: a clean CI run from an empty SDK cache produces a signed AAB with no XA5207.

Verification checklist

Rollback plan

Reverting the TargetFramework back to net9.0-android restores the old target level and the old .NET MAUI safe area behaviour, and it is a clean revert as long as you did not also adopt .NET 10 APIs. What you cannot roll back is the Play side: once you have shipped an AAB at target 36 you cannot publish a lower target level to the same track afterwards, because Play enforces the floor on every new upload. Treat the internal track as your rollback window and the production promotion as one-way.

Gotchas that cost real time

Sources

Comments

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

< Back