Start Debugging

Migra una app .NET MAUI para Android al nivel de API 36

Google Play exige el nivel de API objetivo 36 desde el 2026-08-31, con prórrogas hasta el 2026-11-01. Este es el camino completo en .NET MAUI desde net9.0-android hasta API 36: el cambio de target framework, el uses-sdk fijo que te deja en silencio en el nivel anterior, el modo edge-to-edge sin opción de exclusión, el gesto de retroceso predictivo y las reglas de pantallas grandes.

El cambio en la compilación es una línea. Los cambios de comportamiento son la migración. Google Play empezó a exigir el nivel de API objetivo 36 para apps nuevas y actualizaciones el 2026-08-31, con una prórroga por app disponible en Play Console hasta el 2026-11-01, así que si esta semana te rechazaron una actualización, esta es la razón. En una app .NET MAUI el nivel de API objetivo no es un ajuste del manifiesto que edites: se deriva de la versión de la plataforma Android en tu TargetFramework, y .NET 9 llega como máximo a API 35. Eso significa que esto es una actualización del SDK de .NET a .NET 10 (o .NET 11), no un retoque del manifiesto. Calcula un día para una app pequeña y un sprint para cualquiera que tenga orientación bloqueada, un botón de retroceso personalizado o insets ajustados a mano. Esta guía apunta a .NET 10 con .NET MAUI 10.0.100 (publicado el 2026-08-20) como destino, e indica en qué se diferencia .NET 11.

Por qué Play revisa el nivel objetivo, y no otro

Qué se rompe

ÁreaCambio con el objetivo API 36Severidad
Edge-to-edgewindowOptOutEdgeToEdgeEnforcement queda obsoleto y se ignora en dispositivos con Android 16alta
Áreas seguras de .NET MAUIContentPage.SafeAreaEdges vale None por defecto desde .NET 10, así que las páginas van de borde a bordealta
Retroceso predictivoLas animaciones de vuelta al inicio y entre actividades están activas por defecto; OnBackPressed no se llamaalta
Pantallas grandesandroid:screenOrientation, resizableActivity, minAspectRatio y maxAspectRatio se ignoran a partir de sw600dpalta (tablets, plegables)
SDK de .NETAPI 36 necesita net10.0-android o posterior; la carga de trabajo de .NET 9 se detiene en API 35alta
API mínima.NET 11 sube el mínimo de API 21 a API 24media (solo .NET 11)
Renderizado de textoandroid:elegantTextHeight queda obsoleto y se ignorabaja
Programación de tareasScheduledExecutorService.scheduleAtFixedRate repite como máximo una ejecución perdidabaja
Sensores de saludBODY_SENSORS se sustituye por permisos granulares android.permissions.healthbaja (salvo que leas la frecuencia cardiaca)

Las dos primeras filas se combinan. Actualizar a .NET 10 para conseguir API 36 también cambia el valor por defecto de las áreas seguras del propio .NET MAUI en el mismo commit, así que una app que se veía bien en .NET 9 con objetivo 35 puede salir del proceso con la barra de título debajo de la barra de estado por dos motivos independientes.

Lista de comprobación previa

Pasos de migración

  1. Averigua a qué apuntas hoy realmente. No leas el csproj, lee el manifiesto combinado que produce la compilación:

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

    Verificación: obtienes un único número. Si es menor que la versión de plataforma Android de tu TargetFramework, algo lo está fijando, y el paso 3 es el que más importa en tu caso.

  2. Mueve el target framework a .NET 10. La versión de plataforma Android del TFM es lo que se convierte en targetSdkVersion, así que esta única edición es la migración real:

    <!-- .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 a secas se resuelve a API 36, que es el valor por defecto documentado de .NET 10. Fíjalo de forma explícita como net10.0-android36.0 si prefieres que la compilación falle en lugar de desplazarse cuando más adelante pases a .NET 11, porque .NET for Android promovió API 37 a estable en .NET 11 Preview 5 y ahora los proyectos .NET 11 apuntan por defecto a net11.0-android37. $(SupportedOSPlatformVersion) es un eje distinto: se convierte en minSdkVersion y no tiene nada que ver con el requisito de Play.

    Verificación: vuelve a compilar y repite el grep del paso 1 contra obj/Release/net10.0-android/AndroidManifest.xml. Debe imprimir targetSdkVersion="36".

  3. Elimina cualquier uses-sdk fijo de tu manifiesto. Esta es la causa más común de que el paso 2 parezca no hacer nada. .NET for Android solo escribe targetSdkVersion cuando la plantilla del manifiesto no tiene ya uno, y un valor explícito gana sin discusión (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>

    La propia guía de XA5207 de Microsoft indicaba añadir exactamente este elemento para conservar un nivel objetivo durante una actualización del SDK, así que muchos proyectos de la época de Xamarin.Forms todavía lo arrastran. La plantilla actual de .NET MAUI no incluye ningún elemento uses-sdk, que es el estado que quieres.

    Verificación: grep -c uses-sdk Platforms/Android/AndroidManifest.xml devuelve 0, y el manifiesto combinado sigue mostrando targetSdkVersion="36".

  4. Decide tu estrategia de edge-to-edge, porque ya no tienes voto. Con objetivo 36 el atributo windowOptOutEdgeToEdgeEnforcement está obsoleto y deshabilitado en dispositivos con Android 16. Si lo tenías en Platforms/Android/Resources/values/styles.xml, bórralo. Después elige un valor de SafeAreaEdges por página en lugar de aceptar el valor por defecto de .NET 10, que es 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 reproduce el comportamiento de .NET 9 de mantenerse fuera de las barras del sistema y de los recortes de pantalla. All además evita el teclado, que es lo que quieres si dependías del platform-specific WindowSoftInputModeAdjust.Resize de Android. None es la opción inmersiva, y es una decisión deliberada, no un valor por defecto que debas heredar por accidente.

    Verificación: en un dispositivo con Android 16, la barra de estado y la barra de navegación por gestos no se superponen a ningún control pulsable en tus tres pantallas principales, en tema claro y oscuro.

  5. Arregla el manejo personalizado del retroceso antes de que el retroceso predictivo se lo coma. Con objetivo 36 las animaciones de retroceso predictivo están activas por defecto, onBackPressed() no se llama y KeyEvent.KEYCODE_BACK no se despacha. Cualquier sobrescritura de actividad como esta deja de ejecutarse:

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

    Trátalo en la superficie de navegación propia de .NET MAUI, que sigue funcionando en todas las plataformas:

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

    La salida de emergencia de Android es android:enableOnBackInvokedCallback="false" en <application> o en una sola <activity>, y es un parche temporal, no una solución.

    Verificación: desliza desde el borde de la pantalla y mantén. Deberías ver la animación de anticipación, y al soltar debería ocurrir lo que tu manejador pretende.

  6. Audita la orientación bloqueada y las relaciones de aspecto fijas. En pantallas de sw600dp o más, el objetivo 36 hace que Android ignore android:screenOrientation, android:resizableActivity, android:minAspectRatio y android:maxAspectRatio, junto con SetRequestedOrientation en tiempo de ejecución. En .NET MAUI eso suele significar un atributo en MainActivity:

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

    La exclusión temporal es una propiedad del manifiesto, y Google ha indicado que deja de aplicarse en el nivel de API 37:

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

    Verificación: ejecuta la app en un emulador de tablet o plegable y rota. Si el diseño es inservible en horizontal, arregla el diseño, porque la exclusión solo te compra un año.

  7. Actualiza CI para que no compile contra una plataforma que no tiene. Que falte API 36 en un agente aparece como XA5207, y la solución es un target, no una descarga desde un portal:

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

    El argumento -f es obligatorio; si no, MSBuild informa MSB4057: The target "InstallAndroidDependencies" does not exist in the project.

    Verificación: una ejecución limpia de CI desde una caché vacía del SDK produce un AAB firmado sin XA5207.

Lista de verificación

Plan de reversión

Revertir el TargetFramework a net9.0-android restaura el nivel objetivo anterior y el comportamiento anterior de las áreas seguras de .NET MAUI, y es una reversión limpia siempre que no hayas adoptado además APIs de .NET 10. Lo que no puedes revertir es el lado de Play: una vez que has publicado un AAB con objetivo 36, después no puedes publicar un nivel objetivo menor en el mismo canal, porque Play aplica el mínimo en cada subida. Trata el canal interno como tu ventana de reversión y la promoción a producción como algo de un solo sentido.

Detalles que cuestan tiempo real

Relacionado

Fuentes

Comments

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

< Volver