Как стать автором
Обновить

Как и зачем мы встраивали одно Android-приложение в другое

Время на прочтение8 мин
Количество просмотров3.1K

Немного терминологии

  • МП — мобильное приложение

  • SDK — Software Development Kit как термин, а также, в рамках данной статьи будем так называть наше встраиваемое мобильное приложение

  • Целевое МП — МП, в которое был интегрирован наш SDK

Зачем?

Итак, представим ситуацию:

У заказчика есть своя экосистема и несколько приложений. В качестве MVP внутри этой экосистемы разрабатывается новое приложение. После запуска приложение показывает хороший результат, и его решают развивать. Через некоторое время заказчик принимает решение расширить функционал своих существующих приложений за счет недавно запущенного. Иначе говоря, упаковать это приложение в SDK и встроить в другие.

Как вы понимаете, в данном случае, SDK — не единичный Fragment/Activity и не набор утилит — это несколько десятков экранов с кучей бизнес-логики, сетевая прослойка, БД, и пара специфических фич, завязанных на камере смартфона.

Итак, представим ситуацию: приложение N было запущено как отдельный бизнес-кейс, написано с нуля и выложено в сторы. Затем, заказчик, у которого есть собственный пул мобильных приложений со своими командами разработки, решил интегрировать приложение N внутрь других приложений, расширив таким образом функционал. Все эти приложения существуют в одной экосистеме.

Отличия от обычного SDK

Начнем с того, что обычно любой SDK сразу планируется как отдельный встраиваемый модуль/библиотека. Естественно, это влияет на архитектуру проекта и на его сторонние зависимости (чем меньше “левых” библиотек, тем лучше).

В нашем случае никто не предполагал, что придется интегрироваться в другие приложения, и проект разрабатывался по обычным стандартам.

Во многом нам повезло со стеком технологий и сторонними зависимостями — почти все используемые библиотеки перенесли интеграцию без больших сложностей. Например, Dagger 2 практически не создал нам проблем (хотя перед этим пришлось переделать всю инициализацию графа). Яндекс.Карты и Яндекс.Метрика были как в нашем приложении, так и в целевом МП, при этом их инстансы работали независимо и без проблем. А вот от Firebase в SDK пришлось отказаться — эти библиотеки Google не рассчитаны на запуск двух инстансов сразу.

Как?

В процессе интеграции нам пришлось решить множество проблем, некоторые были простыми, некоторые имели неочевидное решение, а с некоторыми мы вообще сталкивались впервые. Сейчас расскажем подробнее.

Запуск SDK и его жизненный цикл

В первую очередь пришлось избавляться от Activity, на которой изначально строилось наше приложение. Интегрировались мы как встраиваемый фрагмент, и максимально ограничивали внешние воздействия. Хотя, в некоторых случаях вызовы к Activity сохранились. Естественно, все фичи из Activity пришлось переносить внутрь фрагмента (например, срабатывание вибрации), благо, всё удалось без проблем.

Сам SDK запускается легко — достаточно создать объект-конфигуратор и передать его в метод, получив результат в виде объекта фрагмента. В момент старта инициализируется граф зависимостей (отдельно от графа целевого МП) и подключаются все необходимые коллбэки. Дальше SDK живёт своей жизнью, практически не связываясь с целевым МП.

В реализации подключения SDK есть один важный момент — большинство запросов в сеть требуют авторизацию, при этом сам SDK токены не хранит и не получает — это делает то приложение, в которое SDK интегрирован. Такой способ доставки токенов был выбран потому, что у SDK и целевых МП общий формат токена, а вот способ авторизации — разный, поскольку у каждого приложения свой бэк и БД с юзерами.

Проблемы версионности

Процесс разработки заметно усложнял тот факт, что мы во время разработки SDK должны были параллельно поддерживать сразу две версии нашего старого приложения: одна версия — текущий релиз в маркете, где нужно фиксить баги и добавлять небольшие фичи, вторая версия — отдельная сборка с поддержкой перевода на английский. Причем позже фича мультияза должна была перекочевать в релизную сборку. При этом параллельно добавляется переработка приложения в SDK. Согласитесь, нетривиальная ситуация?

В процессе превращения обычного приложения в компилируемый файл библиотеки, мы, конечно, не обновляли кодовую базу. И спустя некоторое время сложилась следующая ситуация:

Естественно, такой разброс приводил к проблемам со слиянием, поддержкой актуального состояния и тестированием. Но, в конечном итоге, мы всё благополучно объединили в одной ветке.

В идеальном мире мы бы заморозили разработку новых фич, доработали мультиязычность и довели SDK до релиза, вернувшись к выпуску фич позже, но такой возможности не было.

Вероятно, если бы проект был мультимодульным с разделением по фичам, то ситуация была заметно лучше — перенос нового функционала занимал бы меньше времени.

Работа с GooglePay

SDK позволяет проводить оплату с помощью GPay (предвосхищая вопрос — Huawei Pay пока не поддерживаем), однако для запуска экрана оплаты необходимо выполнить метод класса AutoResolveHelper.resolveTask, чтобы затем получить результат внутри метода Activity.onActivityResult, а, как вы помните, в SDK у нас нет ни одной активити!

Так что эта задача легла на плечи разработчиков МП, в которые мы интегрировались. В SDK мы добавили метод, в который нужно передавать Intent — результат запроса к GPay, и всё заработало без проблем и с минимальными изменениями. Спасибо Google за простую интеграцию.

//Получаем результат в Activity приложения
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {           
    super.onActivityResult(requestCode, resultCode, data)       
    SDK.handleGooglePayRequestResult(requestCode, resultCode, data)
}
// Внутри SDK
fun handleGooglePayRequestResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {   
    if (requestCode == GOOGLE_PAY_REQUEST_KEY) {           
         when (resultCode) {
            Activity.RESULT_OK -> {
                val token = parseData(data) /*Парсим результат*/
                googlePayResultListener?.onSuccess(token)
            }                
            Activity.RESULT_CANCELED -> {
                googlePayResultListener?.onCancel()
            }
            AutoResolveHelper.RESULT_ERROR -> {
                AutoResolveHelper.getStatusFromIntent(data)?.let { status ->
                    googlePayResultListener?.onError(status)
                }                
            }            
        }   
        // Если результат обработан в SDK         
        return true        
    }
    // Если результат не предназначен для SDK        
    return false    
}

Доставка зависимостей и проблемы разных архитектур

Как уже писали выше, нам во многом очень повезло с зависимостями в приложениях. Самый главный плюс — во всех МП, в которые мы интегрировались, как и в нашем SDK, использовались Яндекс.Карты. Не пришлось перерабатывать экраны с картой, и не пришлось добавлять лишние зависимости. Однако, помимо этого были следующие проблемы:

  1. SDK на корутинах, целевое МП на RxJava — из-за разницы в подходах и архитектурах мы старались сделать так, чтобы SDK по возможности вообще никак не взаимодействовал с внешним кодом. Но коллбэки писать всё же пришлось. В некоторых случаях было бы очень круто использовать всю мощь корутин, но у приложений были разные стеки библиотек, так что мы использовали старые добрые слушатели.

  2. Breaking changes в разных версиях библиотек — Room 2.3.0 в SDK и 2.2.5 в целевом МП. Пришлось понижать версию в SDK. Почему всё ломалось, мы так и не поняли, поскольку серьезных изменений в рамках этих обновлений не было.

  3. Отсутствие репозитория для SDK — на этапе разработки не было возможности использовать maven-репозиторий для доставки зависимостей, так что приходилось поставлять .aar файл, и заодно список всех используемых библиотек, поскольку .aar сборки сторонние зависимости не хранят.

Ресурсы и слияние манифестов

Больше всего проблем было именно с ресурсами. Если в SDK и целевом МП были файлы ресурсов с одинаковым именованием, то при сборке Android оставлял только тот файл, который был в целевом МП. Из-за этого как минимум два раза сталкивались с хитрыми багами, которые долго вычисляли. А еще несколько иконок были заменены подобным образом, что мы заметили не сразу.

Сюда же можно отнести проблему стилей. У целевого МП была собственная тема для Activity, у нас собственная. Пришлось по итогу вынести запуск SDK в отдельную активити, чтобы не было конфликтов наследования в темах — некоторые параметры у нас отличались.

Тестирование

Главная сложность тестирования заключалась в том, что команд, как и приложений, несколько. Каждая поломка отличалась от предыдущей, и нельзя было просто залезть в чужой код и продебажить его — далеко не всегда есть возможность заглянуть в другой проект.

Поэтому в случае выявления бага приходилось сначала его воспроизводить. Иногда это получалось, и начинался дебаггинг с дальнейшими правками. После этого собиралась новая версия SDK, передавалась разработчикам целевых МП, они тестировали у себя, и закрывали таск. Конечно же, иногда у них баг повторялся даже после правок, и мы всё начинали сначала.

Например, одной из первых проблем стало исчезновение иконок на кнопках зума карты. Понять удалось не сразу, ведь эту часть кода в процессе интеграции никто не трогал. Оказалось, что у приложения, куда встраивался SDK были ресурсы с таким же именем, но другого цвета, из-за чего ресурсы перезаписывались при слиянии кода и сливались с фоном кнопки. В дальнейшем у нас также возникали похожие проблемы перезаписи файлов.

А когда мы не могли воспроизвести баг на своей стороне, то начиналась игра "Угадай ошибку по логам". В такой ситуации скорость решения очень сильно зависела от того, насколько опытен разработчик, который брался за фикс. К счастью, мы справились со всеми подобными случаями.

Так, однажды, нам пришлось чинить баг, который воспроизводился только на группе устройств (привет, смартфоны Huawei), из данных только логи, в которых ошибка движка chromium без конкретной точки срабатывания. После мозгового штурма удалось выяснить, что мы использовали неверный объект Context при инициализации модуля переключения языка из-за чего приложение падало. Спасибо неизвестному разработчику за сэкономленное время и нервы.

Из-за особенностей тестирования скорость фиксов была низкой — коммуникации между командами не мгновенные, с момента сборки версии SDK с фиксом до момента теста в целевом МП могли пройти часы. Поэтому мы старались максимально протестировать на своей стороне, чтобы снизить шансы на возврат таска.

Советы

Если вдруг вам придется столкнуться с подобным случаем разработки SDK, то может быть вы сэкономите время и нервы, прочитав данную статью. Постараемся кратко описать, на что стоит обращать внимание при старте разработки.

Если вы пишете SDK с нуля, и планируется много UI-фич:

  1. Именуйте файлы ресурсов так, чтобы шанс совпадения названий был минимален. Например, используйте приставку в названии файла (cool_sdk_fragment_main вместо fragment_main).

  2. Минимизируйте количество сторонних библиотек, чтобы снизить шанс конфликтов версий.

  3. Заранее проверьте, будет ли работать конкретная библиотека в рамках SDK. Например, Яндекс.Метрика может иметь несколько репортеров для отправки аналитики, а Firebase нет. При этом, если МП, в которое вы интегрируетесь, будет использовать Firebase Perfomance Monitoring, то Яндекс.Метрика приведет к крэшу в рантайме.

  4. Сведите к минимуму контакт с кодом целевого МП — чем меньше точек соприкосновения, тем меньше работы по интеграции как для вас, так и для разработчиков целевого МП.

  5. Логируйте работу SDK насколько это возможно — особенно в точках соприкосновения с целевым МП — это будет практически единственный инструмент дебаггинга после добавления SDK в сторонний проект.

Если вы переделываете существующее приложение в SDK, то всё то же, что и выше, плюс:

  1. Старайтесь не выпускать новые фичи с момента начала переделки в SDK, чтобы не пришлось потом тратить кучу времени на слияние и тестирование. Если же у вас нет выбора, и новые фичи придется делать параллельно переделке, то хотя бы постарайтесь производить переделку в SDK поэтапно, чтобы можно было периодически сливать новые фичи в ветку разработки SDK. Легче сказать, чем сделать, но всё же попробуйте.

  2. Скорее всего, вам потребуется переделать весь DI. Даже если вам кажется, что это не так, лучше заложите время с учётом, что всё же придется.

Дальнейшее развитие SDK

Мы провели основные работы по превращению приложения в SDK и его интеграции в другие приложения. Но у нас еще осталось много работы — рефакторинг слабых мест, уменьшение объема (размер целевых МП на Android вырос в полтора раза, а на IOS вообще в два), детальное логирование работы SDK, множество мелких правок. А там не за горами выпуск новых фич.

Вы дочитали до конца? Поздравляем! Надеюсь, наш опыт разработки и интеграции одного мобильного приложения в другое поможет кому-то еще и упростит такую нелегкую задачу.

Теги:
Хабы:
+2
Комментарии3

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн