API обновлений внутри приложения Android в 2026 году: гибкое или немедленное, и когда его форсировать
Практическое руководство по In-App Update API от Google: гибкий и немедленный сценарии, приоритет обновления, дни устаревания и проверка возобновления, о которой все забывают.
Автообновление Play Store рано или поздно охватывает большинство пользователей, но «рано или поздно» — ключевые слова в этой фразе. Настройка «только Wi-Fi», нехватка места на диске или пользователь, который просто никогда не открывает приложение Play Store, могут оставить кого-то на три версии позади — всё ещё на сборке с багом, который вы исправили две недели назад, или, что хуже, всё ещё упирающимся в серверный контракт, который ваш бэкенд больше не поддерживает. In-App Update API от Google существует именно для этого разрыва: он позволяет вашему приложению проверить наличие новой версии и предложить обновление прямо изнутри, не отправляя пользователя никуда.
У этого API два разных сценария, и выбор не того — самый распространённый способ реализовать это плохо.
Как это работает на самом деле
API входит в Play Core (com.google.android.play:app-update-ktx). Вы запрашиваете у AppUpdateManager текущий AppUpdateInfo, который сообщает, доступно ли обновление и что Google о нём знает:
val updateManager = AppUpdateManagerFactory.create(context)
updateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
// decide which flow to start based on info.updatePriority()
// and info.clientVersionStalenessDays()
}
}
Этот объект AppUpdateInfo — практически весь API; всё остальное сводится к решению, что с ним делать.
Гибкое и немедленное: две разные задачи
Гибкое обновление скачивается в фоне, пока пользователь продолжает пользоваться приложением, а затем, когда оно готово, показывает небольшой снэкбар «перезапустить для обновления». Пользователь не заблокирован и может закрыть подсказку. Это правильный выбор по умолчанию для всего не срочного — косметическая правка интерфейса, новая функция, неломающее исправление бага.
if (info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
)
}
Когда загрузка завершена, InstallStateUpdatedListener сообщает InstallStatus.DOWNLOADED, и вы явно завершаете установку:
updateManager.registerListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
updateManager.completeUpdate() // shows the snackbar's restart action
}
}
Немедленное — это противоположность: полноэкранный блокирующий сценарий, который не даёт пользователю пользоваться приложением, пока обновление не установится. Он разрушителен по замыслу, а значит, предназначен ровно для одной ситуации — текущая версия сломана так, что это действительно важно. Исправление безопасности, миграция схемы Room, которую старый клиент повредит, изменение backend API, из-за которого старая сборка падает на каждом запросе. Если вы не можете объяснить, почему позволить кому-то использовать старую версию ещё один день реально вредно, это не немедленное обновление, а гибкое.
if (info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
Я прибегал к этому лишь однажды — выпуская релиз Granyn, который менял способ хранения повторяющихся транзакций: старый клиент, записывающий в новую схему, молча исказил бы балансы. Это стоит блокировки. Переименованная кнопка — нет.
Определение приоритета (и устаревания)
AppUpdateInfo даёт вам два сигнала вместо того, чтобы оставлять выбор между гибким и немедленным на угадывание:
updatePriority()— целое число от 0 до 5, которое вы сами задаёте для каждого релиза в Play Console во время выката. Оно не вычисляется из диффа — именно вы сообщаете Google (и своему клиентскому коду), насколько срочен этот релиз.clientVersionStalenessDays()— сколько дней обновление доступно именно этому пользователю, что отличается от того, когда вы его выпустили. Поэтапный выкат означает, что это число варьируется по вашей базе установок даже для одного и того же релиза.
Схема, которая хорошо работала для меня: считать приоритет 4-5 годным для немедленного обновления, а для всего остального комбинировать приоритет с устареванием — релиз с приоритетом 3 становится немедленным только после недели-двух без обновления, давая пользователям период отсрочки перед тем, как их прервать.
val shouldForce = info.updatePriority() >= 4 ||
(info.updatePriority() == 3 && (info.clientVersionStalenessDays() ?: 0) > 14)
Проверка, о которой все забывают: возобновление зависшего обновления
Немедленное обновление может быть прервано — пользователь сворачивает приложение, процесс завершается, поступает звонок. Когда это происходит, Play Core не возобновляет его за вас молча; нужно проверять зависшее обновление при каждом возобновлении соответствующего экрана, а не только один раз при запуске:
override fun onResume() {
super.onResume()
updateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
}
}
Пропустите это — и получите сценарий отказа, из-за которого у этого API дурная репутация: пользователя прерывают посреди обновления, он сворачивает приложение, чтобы ответить на сообщение, возвращается — и приложение просто… снова работает, наполовину обновлённое, в состоянии, которое вы никогда не тестировали. Проверка в onResume — это три строки, и именно она отличает надёжное немедленное обновление от прерывистого бага, который вы не можете воспроизвести.
Тестирование без ожидания реального выката
Вы не можете запустить настоящее обновление со своего устройства против своей же текущей установленной сборки — Play Core нужна реальная разница версий, видимая вашему аккаунту. Практичная настройка — внутренний тестовый трек: установите оттуда старый код версии, затем опубликуйте новый и дайте API увидеть реальную разницу. Google также предлагает API тестирования обновлений внутри приложения для локальной имитации ответов AppUpdateInfo — если вы часто касаетесь этого сценария, стоит подключить его к отладочному меню: это гораздо быстрее, чем реальный цикл через трек, для итераций именно над логикой приоритета/устаревания.
Относитесь ко всей этой функции как к подстраховке, а не как к UX-паттерну, на который можно полагаться. Лучшая версия этого API — та, которую ваши пользователи никогда не увидят, потому что автообновление уже сделало свою работу; гибкое и немедленное существуют для дней, когда это не сработало.
// По теме
Ещё из журнала
androidx.startup в 2026 году: порядок инициализации библиотек без стопки ContentProvider'ов
Практическое руководство по библиотеке App Startup для Android — объединение initializer'ов в один ContentProvider, объявление зависимостей и случаи ленивой инициализации, которые она не заменяет.
CameraX в 2026 году: как привязать Preview и ImageAnalysis, не утекая сессией камеры
Практическое руководство по CameraX на Android: привязка Preview и ImageAnalysis к жизненному циклу, правильная стратегия backpressure и краш, который вызывает поворот экрана.
Android Photo Picker API в 2026 году: прикрепить фото без доступа ко всей галерее
Практическое руководство по Photo Picker API в Android — одиночный и множественный выбор, фильтрация по MIME, бэкпорт для версий до Android 13 и почему это лучше READ_MEDIA_IMAGES.