Совсем скоро, 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 сентября, и до скорой встречи!

Комментарии (8)


  1. nebularia
    25.08.2022 18:32
    +22

    Может хватит уже суперприложений? Это прикольно, когда есть одно такое, как какой-нибудь WeChat в Китае, но с этой модой теперь в каждом приложении есть всё. Даже у одной VK их несколько. И место это всё занимает - немеряно, да и работает так себе.


    1. pOmelchenko
      25.08.2022 18:48

      Они же все ради тебя стараются, а ты жалуешься... ага


    1. Xeldos
      26.08.2022 09:49

      Nero burning ROM. Я не по ню, антивирус в нем успел появиться?


  1. md_backend_binance
    25.08.2022 20:31

    Интересный момент , у вас все сектора лежат явно на разных серверах , каждый со своим API , при этом у вас есть "избранное" , куда может попасть любой тип , хранится в виде списка для каждого пользователя как [{type:1, id:222},{type:3, id:345},{type:6, id:868}], при этом есть фильтр по общим чертам как tag, как у вас это работает? У вас есть отдельный сервис как "Сага" который загружает список избранного , потом делает запросы на все сектора api/messages/Get[ids], api/calendar/Get[ids] , после загрузки фильтруете по тэгам и отдаете клиенту ?

    2) И второй вопрос в котором не разобрался , у вас на всех секторах есть допустим лайки , у вас получается полное дублирование кода-базы? как /calendar/like /comment/like /foto/like


  1. atconnect
    26.08.2022 01:31
    +4

    Вы забыли интегрировать браузер Амиго... )


  1. Areso
    26.08.2022 10:20

    На фоне сторис в банковских приложениях, наличие второй почты, календаря и чего-то там ещё не выглядит супероверкиллом.


  1. Vorchun
    26.08.2022 10:31
    +1

    Мне кажется, что это всё "мода". И мода на СуперАпп проходит. Мне как пользователю хочется чтобы приложение выполняло свою функцию. Яндекс.Такси, новый 2ГИС - это что-то страшное.


  1. md_backend_binance
    26.08.2022 23:23
    +1

    А тут отвечают на комменты или это просто была статья пиара - ляпнул и ушел? )