Абсолютное большинство мобильных приложений имеет интересный нюанс – «хвост» старых версий, которыми все еще продолжают пользоваться. В этой статье мы посмотрим, какие проблемы это приносит и как с этим бороться. Материал будет полезен и мобильным разработчикам, и тем, кто каким-либо образом связан с разработкой мобильных приложений, к примеру, разрабатывает backend-сервисы, которые используются в приложениях.
Что такое хвост старых версий и какие от него проблемы?
Допустим, пару лет назад вы выпустили версию 1.0.0, с тех пор произошло еще 30 релизов и текущая актуальная версия приложения в App Store или Google Play у вас, к примеру, 2.5.1. Если вы проанализируете логи обращений к серверу или посмотрите в систему аналитики, то увидите, что предыдущими версиями приложения всё ещё пользуются. Хвост старых версий – это те версии приложения, которые не просто установлены на телефонах пользователей, этими версиями продолжают пользоваться.
Если вы хотите увидеть динамику распределения версий по времени, можете построить графики на основе логов. Если у вас в приложении используется Firebase, подобный график можно посмотреть в виджете «Users by App version over time» из раздела Dashboard.
Я вижу 3 основных группы проблем, которые создает подобный хвост:
1. Баги в текущих версиях приложения
В работе версий приложения, которые все еще встречаются у пользователей, есть баг (никогда такого не было и вот опять). Это может быть креш или какой-то плавающий баг в одной из фич, который приводит к некорректной работе приложения.
Даже если вы уже выложили новую версию с правкой, это не значит, что её уже успели скачать. «Довольные» пользователи нагружают вашу службу поддержки, пишут отзывы в App Store и Google Play, делятся негативом в социальных сетях.
2. Проблемы обеспечения обратной совместимости
Старые версии приложений могут использовать старые версии серверного API, значит нужно поддерживать обратную совместимость, а это далеко не всегда просто и бесплатно. В какой-то момент команда, отвечающая за backend, может сказать, что длительная поддержка разных версий API доставляет им сложности при разработке и тестировании. При этом неважно, каким из путей пойти: версионировать API или пытаться организовать ветвление if-else в коде имеющихся методов.
3. Недостаточно быстрое внедрение фич
В новых версиях приложения добавлен функционал, могут быть важны данные аналитики или доход, который этот функционал приносит, вот только пользователи не торопятся обновляться, приходится ждать. При таком ожидании мы можем терять время и деньги.
Форсируем обновления
Посмотрим на варианты форсирования обновлений мобильных приложений. Учтите, что время не стоит на месте – на момент прочтения этой статьи могут быть и другие инструменты, поэтому перед тем, как возьметесь за реализацию, проведите небольшое исследование.
1. Готовые решения
Для форсирования обновлений существуют готовые библиотеки. К примеру, на iOS такой библиотекой является Siren (ранее Harpy). При запуске приложения номер актуальной версии приложения будет получен из App Store. На основе выбранных вами правил пользователю будет показаны варианты: обновиться сейчас, пропустить новую версию или отказаться от обновления.
Библиотека существует с 2015 года, имеет тысячи звезд на Github, поддерживает локализацию на десятки языков, устанавливается через SPM и CocoaPods, а еще проверяет на возможность обновиться на новую версию с поправкой на текущую версию iOS на устройстве (к примеру, если новая версия приложения требует iOS 13, а на устройстве iOS 12, возможно, пользователю бесполезно сейчас предлагать скачивать обновление).
Решение довольно простое и надежное, рекомендую рассмотреть его, если у вас нет необходимости или желания управлять номерами версий, на которые нужно обновиться, и вы просто хотите, чтобы пользователям всегда предлагалась самая свежая версия. Библиотеки, аналогичные по функционалу Siren, есть и на Android, к примеру AppUpdater.
Для Android-приложений можно воспользоваться инструментом обновления от Google in-app updates. Он тесно интегрирован с Google Play, относительно прост в использовании, требует для своей работы Play Core library. In-app updates поддерживает 2 сценария обновления.
Сценарий Flexible updates позволяет запустить скачивание обновления в фоне, не прерывая взаимодействия пользователя с приложением, с точки зрения UX это очень хорошо. Второй сценарий – Immediate updates потребует от пользователей скачать и установить обновление, прежде чем продолжить использование приложения. Про его работу на хабре уже есть обзорная статья.
2. Самописные решения
Допустим, мы выпускаем новые версии приложений очень часто и не хотим тревожить пользователя предложениями обновиться, когда в этом нет крайней необходимости, или мы хотим управлять процессом «раскатки» релизов и не хотим, чтобы приложения сразу при запуске старались обновиться на последнюю версию. В этом случае для форсированного обновления нам бы пригодилась возможность гибко управлять номерами поддерживаемых версий.
Для каждого приложения на стороне сервера мы можем хранить два номера версий – минимальную необходимую и минимальную рекомендуемую. Идентификатором приложения может выступать Package name для Android и Bundle ID для iOS.
При запуске приложения мы будем сравнивать номер его версии с номерами, хранящимися на бэкенде:
Если версия приложения ниже минимальной необходимой, нужно обновление (показываем кнопку перехода в нужный магазин приложений).
Если версия приложения выше минимальной необходимой, но ниже минимальной рекомендуемой, можем предложить пользователю обновление, от которого он может отказаться до следующего запуска или до выхода следующей рекомендуемой версии.
Я предлагаю хранить версии в формате семантического версионирования, а не номеров сборки. Согласен, что сравнение двух целых чисел, которыми обычно характеризуют номера сборки, проще, чем парсинг строк в формате «1.2.17» и «1.5.13» и поочередное сравнение номеров мажора, минора и патча (на все это хорошо бы иметь тесты + на iOS можно использовать стандартную функцию compare), но у семантических версий вижу важные преимущества:
Сотрудники службы поддержки, пользователи, аналитики, проджект и продакт менеджеры редко понимают про номера сборок, им проще посмотреть актуальную версию в Google Play и историю версий в App Store, а также историю релизов в системах аналитики.
Номера сборки не всегда просто переносить между разными сборочными сервисами и машинами, вам придется помнить об этом при настройке CI/CD.
Храним номера версий на своем сервере
Можно хранить номера версий на сервере и отдавать их через API. Для хранения можно использовать базу данных или статичный файл (не забудьте в этом случае поставить короткое время жизни кэша или отключить его). Преимущества файла в том, что он будет работать быстро и не создавать при этом нагрузку на базу, что может быть актуально для приложений с большой аудиторией.
Пример реализации такого подхода можно посмотреть в материале Force-Updating Your Apps Should Be Industry Standard.
Firebase Remote Config
Firebase Remote Config – простой, бесплатный и довольно удобный инструмент для работы с различными фиче-флагами и параметрами. К вашим услугам панель администратора, где можно менять значения параметров, а также клиентский SDK, готовый к внедрению в мобильные приложения.
Номера версий вы можете хранить в Remote Config, пример реализации можно посмотреть тут.
Если в приложении уже есть Firebase, добавить Remote Config очень просто, но важно помнить про следующие нюансы:
Remote Config не предназначен для частого чтения и имеет кэш со своим временем жизни (по умолчанию 12 часов), а значит изменения могут долго добираться до клиентов. Вы сможете управлять временем жизни кэша и использовать более агрессивный (частый) опрос, но в этом случае можете упереться в квоты Firebase. К тому же, если в Remote Config будет много параметров, частое скачивание их полного набора создаст ненужную нагрузку.
В консоли Remote Config есть история изменений – вы сможете увидеть, кто и когда поменял значения, но у вас не получится ограничить доступ определенным пользователям на изменение какой-то части параметров.
Remote Config не всегда стабильно вел себя при тестировании.
Мысли, идеи и советы
Пользователи не всегда хотят или могут скачивать обновления. Им может быть неудобно в данный момент: садится телефон, дорогой или медленный интернет, не хватает места для установки новой версии. Если вы будете затруднять процесс использования ваших приложений частыми запросами на обновления, рискуете вызвать недовольство.
В iOS 13 было снято ограничение на размер приложений для загрузки через сотовую связь, но необходимость обновить приложение более 200 Мб может отпугнуть пользователя.
Если какая-то часть кода приложения очень часто меняется и требует частых обновлений, рассмотрите варианты ее переноса на бэкенд, чтобы ее можно было менять без перевыпуска приложения.
Представьте, что вам пришлось выложить новую версию приложения с другим Package name на Android или Bundle ID на iOS. Фактически это означает выкладку новых приложений в сторы. Механизм обновления перестанет работать – нужно как-то мигрировать пользователей в новые приложения. Для самописных вариантов вы сможете помимо номеров версий заложить отдельное поле с url нового приложения для миграции.
Если у вас есть несколько приложений, которые собираются из общего набора модулей, каждый из которых имеет свою версию, рассмотрите вариант сравнивать версии поддерживаемых модулей, а не приложений.