Start Debugging

Переводим приложение .NET MAUI для Android на целевой уровень API 36

Google Play требует целевой уровень API 36 с 2026-08-31, продления действуют до 2026-11-01. Полный путь .NET MAUI от net9.0-android до API 36: смена target framework, жёстко прописанный uses-sdk, который молча удерживает старый уровень, режим edge-to-edge без возможности отказа, предиктивный жест назад и правила для больших экранов.

Изменение в сборке занимает одну строку. Миграция состоит из изменений поведения. Google Play начал требовать целевой уровень API 36 для новых приложений и обновлений с 2026-08-31, с продлением по каждому приложению через Play Console до 2026-11-01, так что если на этой неделе ваше обновление отклонили, причина в этом. В приложении .NET MAUI целевой уровень API не является настройкой манифеста, которую вы правите: он выводится из версии платформы Android в вашем TargetFramework, а .NET 9 доходит максимум до API 35. Значит, это обновление .NET SDK до .NET 10 (или .NET 11), а не правка манифеста. Заложите день на небольшое приложение и спринт на любое, где есть фиксированная ориентация, собственная кнопка назад или вручную подобранные отступы. Это руководство ориентируется на .NET 10 с .NET MAUI 10.0.100 (выпуск 2026-08-20) как на конечную точку и отмечает, чем отличается .NET 11.

Почему Play проверяет именно целевой уровень

Что ломается

ОбластьИзменение при целевом API 36Серьёзность
Edge-to-edgewindowOptOutEdgeToEdgeEnforcement объявлен устаревшим и игнорируется на устройствах с Android 16высокая
Безопасные области .NET MAUIContentPage.SafeAreaEdges по умолчанию равен None начиная с .NET 10, поэтому страницы идут от края до краявысокая
Предиктивный жест назадАнимации возврата на домашний экран и между активностями включены по умолчанию; OnBackPressed не вызываетсявысокая
Большие экраныandroid:screenOrientation, resizableActivity, minAspectRatio и maxAspectRatio игнорируются начиная с sw600dpвысокая (планшеты, складные устройства)
.NET SDKДля API 36 нужен net10.0-android или новее; рабочая нагрузка .NET 9 останавливается на API 35высокая
Минимальный API.NET 11 поднимает нижнюю границу с API 21 до API 24средняя (только .NET 11)
Отрисовка текстаandroid:elegantTextHeight объявлен устаревшим и игнорируетсянизкая
Планирование задачScheduledExecutorService.scheduleAtFixedRate навёрстывает не более одного пропущенного запусканизкая
Датчики здоровьяBODY_SENSORS заменён гранулярными разрешениями android.permissions.healthнизкая (если вы не читаете пульс)

Первые две строки складываются. Переход на .NET 10 ради API 36 в том же коммите меняет и собственное значение безопасных областей .NET MAUI по умолчанию, поэтому приложение, которое нормально выглядело на .NET 9 с целевым уровнем 35, может выйти из миграции с заголовком под строкой состояния сразу по двум независимым причинам.

Подготовительный список

Шаги миграции

  1. Выясните, какой уровень у вас на самом деле сейчас. Читайте не csproj, а объединённый манифест, который выдаёт сборка:

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

    Проверка: вы получаете одно число. Если оно меньше версии платформы Android в вашем TargetFramework, значит его что-то фиксирует, и шаг 3 для вас важнее всего.

  2. Переведите target framework на .NET 10. Версия платформы Android в TFM и становится targetSdkVersion, так что эта единственная правка и есть миграция:

    <!-- .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. Зафиксируйте явно как net10.0-android36.0, если хотите, чтобы сборка падала, а не уезжала при последующем переходе на .NET 11, потому что .NET for Android перевёл API 37 в стабильные в .NET 11 Preview 5 и теперь проекты .NET 11 по умолчанию нацелены на net11.0-android37. $(SupportedOSPlatformVersion) это отдельная ось: она становится minSdkVersion и к требованию Play отношения не имеет.

    Проверка: пересоберите и повторите grep из шага 1 для obj/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.xml возвращает 0, а объединённый манифест по-прежнему показывает targetSdkVersion="36".

  4. Определитесь со стратегией edge-to-edge, потому что права голоса у вас больше нет. При целевом уровне 36 атрибут windowOptOutEdgeToEdgeEnforcement объявлен устаревшим и отключён на устройствах с Android 16. Если он был в Platforms/Android/Resources/values/styles.xml, удалите его. Затем выберите значение SafeAreaEdges для каждой страницы вместо того, чтобы принимать умолчание .NET 10, равное 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 воспроизводит поведение .NET 9, при котором содержимое держится в стороне от системных панелей и вырезов экрана. All дополнительно уходит от клавиатуры, и это то, что нужно, если вы полагались на платформенную настройку Android WindowSoftInputModeAdjust.Resize. None это иммерсивный вариант, и он должен быть осознанным выбором, а не унаследованным по случайности умолчанием.

    Проверка: на устройстве с Android 16 строка состояния и панель жестовой навигации не перекрывают ни один нажимаемый элемент на трёх ваших основных экранах, в светлой и тёмной темах.

  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 это android:enableOnBackInvokedCallback="false" на <application> или на отдельной <activity>, и он является временной затычкой, а не решением.

    Проверка: проведите пальцем от края экрана и задержите. Вы должны увидеть анимацию предпросмотра, а после отпускания должно произойти то, что задумано вашим обработчиком.

  6. Проверьте фиксированную ориентацию и жёсткие соотношения сторон. На экранах от sw600dp целевой уровень 36 заставляет Android игнорировать android:screenOrientation, android:resizableActivity, android:minAspectRatio и android: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>

    Проверка: запустите на эмуляторе планшета или складного устройства и поверните его. Если в альбомной ориентации макет непригоден, чините макет, потому что отказ покупает вам один год.

  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.

    Проверка: чистый прогон CI с пустым кешем SDK выдаёт подписанный AAB без XA5207.

Контрольный список проверки

План отката

Возврат TargetFramework к net9.0-android восстанавливает прежний целевой уровень и прежнее поведение безопасных областей .NET MAUI, и это чистый откат при условии, что вы не начали заодно использовать API из .NET 10. Что откатить нельзя, так это сторону Play: после публикации AAB с целевым уровнем 36 вы уже не сможете опубликовать более низкий целевой уровень в тот же канал, потому что Play применяет порог к каждой загрузке. Считайте внутренний канал своим окном отката, а перевод в production односторонним действием.

Мелочи, которые стоят реального времени

Связанное

Источники

Comments

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

< Назад