Совсем скоро, 3 и 4 сентября в VK пройдёт новый Weekend Offer. В нём будет участвовать и наша команда — мы создаём суперприложение на основе почтового клиента Mail.ru. Хотим подробнее рассказать об этом проекте и о задачах, которые нужно будет решать нашим будущим коллегам :)
Год назад бизнес поставил нам задачу: интегрировать в приложение несколько других сервисов компании, чтобы пользователи могли одним нажатием переходить из сервиса в сервис. Ну, вы и сами знаете, для чего нужны суперы — для развития экосистемы и конкретных продуктов. И спустя два месяца мы запустили в эксплуатацию суперприложение на основе почтового клиента Mail.Ru.
Архитектура
Интегрировать в суперприложение нужно было Облако, Марусю, Новости, Календарь и Задачи, а заодно требовалось создать техническую основу, позволяющую легко и быстро добавлять новые сервисы, как вставить вилку в розетку. Архитектурно мы отказались от конкретного приложения-ядра, и решили сделать так, чтобы хостом суперприложения мог стать любой из мобильных клиентов сервисов.
С точки зрения разработчиков мы хотели сделать универсальный механизм интеграции, чтобы авторам хоста и подключаемых сервисов не нужно было разбираться в устройстве продуктов друг друга, достаточно соответствовать общему набору правил. Задача осложнялась тем, что подключаемые к суперприложению сервисы имели разную реализацию. Кто-то предоставлял готовое SDK (Маруся и Пульс) и достаточно было просто встроить его; какие-то сервисы пришлось подключать через WebView (Календарь и Задачи); а кому-то пришлось писать код с нуля, чтобы интегрироваться в суперприложение (Облако).
Мы пришли к единому интерфейсу адаптера для всех сервисов, который необходимо реализовать, чтобы вкладка появилась в нашем приложении.
Модули
Для перехода на новую архитектуру мы реализовали хост-портал и подключаемые сервисы в виде модулей. В какой-то момент мы вспомнили, что сервисы могут вызывать функции других сервисов. Например, Облако может открыть форму написания электронного письма и приложить к нему какой-нибудь файл. Поэтому сделали отдельными модулями реализации интерфейсов, а позднее — модули с самими интерфейсами.
White label
Также мы подумали о том, что не всем брендам компании могут быть нужны все сервисы, и придумали механизм выборочного исключения из приложения кода определённых вкладок. Для брендов по-отдельности регистрируем поддерживаемые его сервисом функции, и каждый модуль сервиса ссылается только на API-модули и запрашивает доступность определённых функций. Если какая-то функция недоступна, то приложение не показывает соответствующие элементы интерфейса — кнопки, формы и т.д. Например, если в суперприложении не будет вкладки Облака, то не получится приложить облачный файл в форме написания письма в Почте.
Вкладки и фоновые сервисы
Сначала мы считали, что каждый новый сервис будет отображаться в интерфейсе в виде отдельной вкладки. Но потом поняли, что некоторым сервисам вкладка не нужна, они должны работать в фоновом режиме. Самый яркий пример — поиск, это тоже отдельный сервис, но он интегрируется во вкладки других сервисов и не имеет собственной. Пришлось немного переделать интерфейс адаптера.
При переключении вкладок мы не просто подставляем экраны, но и меняем содержимое общих компонентов — верхней и нижней панели: меняется доступный набор действий. То есть в зависимости от выбранного сервиса меняется глобальный контекст суперприложения. Фоновые сервисы этот механизм не используют.
Приоритизация
Чтобы сохранить высокую работоспособность приложения, мы не могли держать в памяти сразу все сервисы. Поэтому придумали систему приоритизирования вкладок: каждому сервису мы вручную выставляем приоритет, и если пользователь переходит с низкоприоритетной вкладки на высокоприоритетную, а затем с неё опять на высокоприоритетную, то мы выгружаем из памяти сервис с низким приоритетом.
Навигация
Выше мы упомянули, что вкладки взаимодействуют друг с другом с помощью API. Но есть и второй механизм — переход по ссылкам, deep link-ам. Он предназначен для навигации пользователей между сервисами. Мы придумали формат ссылок, который должны поддерживать все вкладки, и каждая из них может запросить переход по такой ссылке. Тогда суперприложение открывает экран целевого сервиса и, при необходимости, запускает запрошенную функцию. Например, если вы выберете в Облаке файл и нажмёте кнопку «Отправить в письме», то через deep link Облако обратится к Почте, программа откроет форму написания и прикрепит выбранный файл.
Ещё на основе deep link-ов работают кнопки в push-уведомлениях, чтобы можно было прямо из сообщения одним нажатием перейти в соответствующий сервис и запустить фичу.
Работа с зависимостями
В работе мы столкнулись с тем, что все вкладки так или иначе используют общие библиотеки, у которых могут быть разные версии. Иногда возникали несовместимости: какая-нибудь вкладка обновляла внутри себя некую библиотеку, и в результате приложение могло не собраться, либо в runtime могли возникнуть ошибки. Мы решили это с помощью самописного Gradle-плагина, который валидирует версии зависимостей при сборке проекта (включая транзитивные зависимости). У нас имеется эталонный каталог с заданными версиями зависимостей, с которыми приложение работает стабильно. При сборке приложения плагин проверяет, что в итоговую сборку не попали зависимости с версией выше указанной в каталоге. Если такие зависимости будут обнаружены, то сборка завершится с ошибкой. Внедрение плагина позволило нам контролировать обновления библиотек и избежать неявного поднятия их версии.
Плагин проверяет каждую зависимость:
class DependenciesPlugin: Plugin<Project> {
override fun apply(project: Project) {
project.configurations.all { action ->
action.resolutionStrategy.eachDependency { rule ->
MailResolveStrategy.verifyDependency(rule)
}
}
}
}
Валидация заключается в поиске эталонной версии в каталоге и сравнении её с текущей версией. Если текущая версия выше, то бросаем исключение.
@JvmStatic
fun verifyDependency(details: DependencyResolveDetails) {
val group = details.requested.group
val name = details.requested.name
val currentVersion: String? = details.requested.version
val catalogVersion: VersionNumber? = findInCatalog(group, name)?.version
if (currentVersion != null && catalogVersion != null) {
val moreRecent = maxOf(VersionNumber.parse(currentVersion), catalogVersion)
if (moreRecent != catalogVersion) {
val msg = "$group:$name with version $currentVersion is not allowed " +
"because its version is greater than version $catalogVersion from the catalog."
throw RuntimeException(msg)
}
}
}
Аналитика
Сейчас мы централизованно собираем телеметрию со всех вкладок, а авторы хост-приложения выбирают, в каком приложении анализировать собранные данные. Так в разы проще, чем собирать отдельно с каждой вкладки и слать в разные инструменты.
Что мы уже выяснили по результатам анализа метрик? Самое главное — пользователи, активировавшие новую версию, остаются на ней, и частота их взаимодействия с продуктом растёт. Судя по опросам, новинка людям понравилась. Опросы подтвердили удовлетворённость обновлением. После запуска супераппа использование не-почтовых сервисов в приложении выросло более чем в 10 раз.
* * *
Друзья, мы приглашаем Android-разработчиков в нашу команду, работы много — нужно развивать и расширять наше суперприложение. Записывайтесь на Weekend Offer, который пройдёт 3 и 4 сентября, и до скорой встречи!