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

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

  • 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, множество мелких правок. А там не за горами выпуск новых фич.

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

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


  1. quaer
    29.10.2021 17:44

    Есть какой-то смысл делать многофункциональные приложения?

    Для разработчика это означает что вообще говоря стоимость приложения растёт, а вот цену поднять вряд ли получится. В случае если надо будет что-то позаимствовать из приложения, скорее всего это будет труднее, потому что внутри будет много связей.

    Для пользователя это означает что нельзя вынести иконки часто используемых опций для быстрого доступа. Более долгий старт. Интерфейс скорее всего будет унифицирован, что может привести к неоптимальности для каждой небольшой части.


    1. qw1
      31.10.2021 12:34

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

      Представьте, у вас сверх-популярное приложение по доставке еды, а вы хотите влезть на рынок такси. Добавляем в приложение еды страничку с такси — и вот оно! Может, кто-то и воспользуется.


  1. 402d
    31.10.2021 11:10

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

    Что мешало сразу отрефакторить приложение com.example.app выделив sdk.part ?

    И еще вопрос. Как я понимаю таких приложений получилось штуки 3 или более. Не боитесь, что одним утром прилетят письма ( Страйк,Страйк,Страйк, Бан акка разработчика ) ?