Перейти к содержимому
Все записи

R8 и ProGuard для Kotlin, Room и Compose: сбой, который случается только в релизе

Почему приложение Android на Room + Compose идеально работает в debug и падает в продакшене, и какие именно правила keep R8/ProGuard ловят это раньше пользователя.

MFKAPPS 4 мин чтения

Сборка, которая проходит все тесты на вашем устройстве, а затем падает у пользователя ровно на той же версии, — один из самых тревожных багов, какие только может подкинуть Android, и у него обычно одна причина: minifyEnabled true. Debug-сборки полностью пропускают shrinking и обфускацию, поэтому всё, чего касается R8, ни разу не проверяется до тех пор, пока релизный APK — тот, что уже лежит в Play Store, — не доберётся до кода, который вы на самом деле никогда не запускали.

Это не аргумент против минификации. Сокращение количества методов в приложении на Kotlin + Compose и удаление неиспользуемого кода реально помогают размеру установки и холодному старту, и Google этого всё больше ожидает. Исправление меньше: знать, какие части кодовой базы R8 не может статически проанализировать, и сказать ему их не трогать.

Почему R8 ломает то, что «явно» работает

R8 решает, что оставить, отслеживая достижимость от точек входа приложения — activity, манифест, всё, что вызывается напрямую. Код, до которого добираются только через reflection, через имя класса, сохранённое как строка, или через библиотеку, восстанавливающую тип во время выполнения, невидим для этого трейса. R8 переименовывает его или удаляет, приложение спокойно компилируется, и сбой проявляется только в тот момент, когда этот reflective-путь реально срабатывает.

Два места, где это реально кусает типичное local-first приложение на Android:

Воркеры WorkManager, создаваемые по имени

Если вы планируете фоновую работу — того рода, что стоит за планированием напоминаний в Hydrame, — ваши подклассы ListenableWorker не вызываются напрямую. WorkManager сохраняет полное имя класса воркера в собственной базе данных и восстанавливает экземпляр через Class.forName, когда работа реально выполняется — иногда спустя часы после того, как процесс приложения, который её запланировал, уже завершился. Обфусцируйте это имя класса — и сбой произойдёт не в момент планирования, а позже, тихо, в виде ClassNotFoundException где-то в глубине диспетчера WorkManager, без единой строки стека, указывающей на ваш код:

# proguard-rules.pro
-keep public class * extends androidx.work.ListenableWorker {
    public <init>(android.content.Context, androidx.work.WorkerParameters);
}

Свежие версии work-runtime уже включают близкое к этому consumer-правило, но фраза «вероятно, библиотека уже это покрывает» — не то, что стоит принимать на веру в релизной сборке. Проверьте, а не предполагайте.

JSON на рефлексии для резервных копий и экспорта

Любое local-first приложение с настоящим потоком экспорта/импорта — того рода, что использует Stocky для резервного копирования данных кладовой, — сериализует поля data class сущностей в JSON и обратно. Если эта сериализация идёт через библиотеку на рефлексии (Gson или Moshi без codegen), ключи JSON по умолчанию — это имена ваших Kotlin-свойств. R8 переименовывает приватные поля, которые считает безопасными для переименования, полученный файл экспорта по-прежнему выглядит нормально, а сбой проявляется только при импорте, когда reflective-читалка ищет имя поля, которого уже нет в урезанном классе. Ни падения, ни ошибки — просто данные, которые тихо не возвращаются.

-keepclassmembers class com.example.app.data.** {
    <fields>;
}
-keepattributes Signature

Ограничьте это своим реальным пакетом data, а не всей кодовой базой — широкое -keep class ** сводит на нет весь смысл минификации.

Тестирование той сборки, которая реально выходит

Единственный надёжный способ поймать это — запускать релизную сборку, а не debug-версию:

buildTypes {
    release {
        isMinifyEnabled = true
        isShrinkResources = true
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}

./gradlew assembleRelease, установите APK на реальное устройство и перед каждой отправкой вручную пройдите каждый ключевой сценарий — запланировать напоминание, экспортировать данные, затем принудительно закрыть приложение и открыть заново, чтобы убедиться, что импорт всё ещё их читает. Это пятиминутная проверка против класса багов, который иначе остаётся невидимым, пока не начнут заполняться отчёты о сбоях в Play Console, — а для приложения без стороннего сборщика крашей это единственная видимость, которую вы получите постфактум.

Ещё одна привычка, которую стоит завести: сохраняйте mapping.txt, который R8 генерирует для каждого релиза (Play Console его запросит, а при загрузке через App Bundle это происходит и автоматически). Без него сбой, который всё же дойдёт до вас в продакшене, — это стек с однобуквенными именами классов и методов, читаемый для R8, но не для вас.

Вывод

Минификация не падает шумно. Она подводит именно на том единственном пути кода, который вы не перепроверили вручную, спустя недели после того, как сборка, доставившая его, прошла всё, что вы всё-таки проверили. Относитесь к любому классу, создаваемому по имени, — воркерам, reflective-сериализаторам, всему, что библиотека восстанавливает из строки, — как к правилу keep, которое вы пишете осознанно, а не как к тому, что, как вы надеетесь, уже покрыто настройками библиотеки по умолчанию.

// По теме

Ещё из журнала

MFKAPPS 4 мин чтения

Room TypeConverters в 2026 году: как хранить enum'ы, даты и списки, не повреждая схему

Практическое руководство по Room TypeConverters на Android — enum'ы, Instant/LocalDate и списки — а также ошибки, которые превращают конвертер в незаметный баг повреждения данных.

#android #engineering #room
MFKAPPS 4 мин чтения

Индексы базы данных Room в 2026 году: находим по-настоящему медленный запрос и исправляем его

Практическое руководство по индексированию базы Room/SQLite на Android — чтение EXPLAIN QUERY PLAN, добавление @Index без угадывания и ошибки, которые незаметно сводят индекс на нет.

#android #engineering #room
MFKAPPS 4 мин чтения

@Relation в Room: запрос данных «один ко многим» на Android без N+1-запросов

Практическое руководство по аннотации @Relation в Room — моделирование данных «один ко многим», таких как категории и записи, без N+1-запросов и ручных join'ов.

#android #engineering #room