Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Немного терминологии
МП — мобильное приложение
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 за простую интеграцию.
Доставка зависимостей и проблемы разных архитектур
Как уже писали выше, нам во многом очень повезло с зависимостями в приложениях. Самый главный плюс — во всех МП, в которые мы интегрировались, как и в нашем SDK, использовались Яндекс.Карты. Не пришлось перерабатывать экраны с картой, и не пришлось добавлять лишние зависимости. Однако, помимо этого были следующие проблемы:
SDK на корутинах, целевое МП на RxJava — из-за разницы в подходах и архитектурах мы старались сделать так, чтобы SDK по возможности вообще никак не взаимодействовал с внешним кодом. Но коллбэки писать всё же пришлось. В некоторых случаях было бы очень круто использовать всю мощь корутин, но у приложений были разные стеки библиотек, так что мы использовали старые добрые слушатели.
Breaking changes в разных версиях библиотек — Room 2.3.0 в SDK и 2.2.5 в целевом МП. Пришлось понижать версию в SDK. Почему всё ломалось, мы так и не поняли, поскольку серьезных изменений в рамках этих обновлений не было.
Отсутствие репозитория для SDK — на этапе разработки не было возможности использовать maven-репозиторий для доставки зависимостей, так что приходилось поставлять .aar файл, и заодно список всех используемых библиотек, поскольку .aar сборки сторонние зависимости не хранят.
Ресурсы и слияние манифестов
Больше всего проблем было именно с ресурсами. Если в SDK и целевом МП были файлы ресурсов с одинаковым именованием, то при сборке Android оставлял только тот файл, который был в целевом МП. Из-за этого как минимум два раза сталкивались с хитрыми багами, которые долго вычисляли. А еще несколько иконок были заменены подобным образом, что мы заметили не сразу.
Сюда же можно отнести проблему стилей. У целевого МП была собственная тема для Activity, у нас собственная. Пришлось по итогу вынести запуск SDK в отдельную активити, чтобы не было конфликтов наследования в темах — некоторые параметры у нас отличались.
Тестирование
Главная сложность тестирования заключалась в том, что команд, как и приложений, несколько. Каждая поломка отличалась от предыдущей, и нельзя было просто залезть в чужой код и продебажить его — далеко не всегда есть возможность заглянуть в другой проект.
Поэтому в случае выявления бага приходилось сначала его воспроизводить. Иногда это получалось, и начинался дебаггинг с дальнейшими правками. После этого собиралась новая версия SDK, передавалась разработчикам целевых МП, они тестировали у себя, и закрывали таск. Конечно же, иногда у них баг повторялся даже после правок, и мы всё начинали сначала.
Например, одной из первых проблем стало исчезновение иконок на кнопках зума карты. Понять удалось не сразу, ведь эту часть кода в процессе интеграции никто не трогал. Оказалось, что у приложения, куда встраивался SDK были ресурсы с таким же именем, но другого цвета, из-за чего ресурсы перезаписывались при слиянии кода и сливались с фоном кнопки. В дальнейшем у нас также возникали похожие проблемы перезаписи файлов.
А когда мы не могли воспроизвести баг на своей стороне, то начиналась игра "Угадай ошибку по логам". В такой ситуации скорость решения очень сильно зависела от того, насколько опытен разработчик, который брался за фикс. К счастью, мы справились со всеми подобными случаями.
Так, однажды, нам пришлось чинить баг, который воспроизводился только на группе устройств (привет, смартфоны Huawei), из данных только логи, в которых ошибка движка chromium без конкретной точки срабатывания. После мозгового штурма удалось выяснить, что мы использовали неверный объект Context при инициализации модуля переключения языка из-за чего приложение падало. Спасибо неизвестному разработчику за сэкономленное время и нервы.
Из-за особенностей тестирования скорость фиксов была низкой — коммуникации между командами не мгновенные, с момента сборки версии SDK с фиксом до момента теста в целевом МП могли пройти часы. Поэтому мы старались максимально протестировать на своей стороне, чтобы снизить шансы на возврат таска.
Советы
Если вдруг вам придется столкнуться с подобным случаем разработки SDK, то может быть вы сэкономите время и нервы, прочитав данную статью. Постараемся кратко описать, на что стоит обращать внимание при старте разработки.
Если вы пишете SDK с нуля, и планируется много UI-фич:
Именуйте файлы ресурсов так, чтобы шанс совпадения названий был минимален. Например, используйте приставку в названии файла (cool_sdk_fragment_main вместо fragment_main).
Минимизируйте количество сторонних библиотек, чтобы снизить шанс конфликтов версий.
Заранее проверьте, будет ли работать конкретная библиотека в рамках SDK. Например, Яндекс.Метрика может иметь несколько репортеров для отправки аналитики, а Firebase нет. При этом, если МП, в которое вы интегрируетесь, будет использовать Firebase Perfomance Monitoring, то Яндекс.Метрика приведет к крэшу в рантайме.
Сведите к минимуму контакт с кодом целевого МП — чем меньше точек соприкосновения, тем меньше работы по интеграции как для вас, так и для разработчиков целевого МП.
Логируйте работу SDK насколько это возможно — особенно в точках соприкосновения с целевым МП — это будет практически единственный инструмент дебаггинга после добавления SDK в сторонний проект.
Если вы переделываете существующее приложение в SDK, то всё то же, что и выше, плюс:
Старайтесь не выпускать новые фичи с момента начала переделки в SDK, чтобы не пришлось потом тратить кучу времени на слияние и тестирование. Если же у вас нет выбора, и новые фичи придется делать параллельно переделке, то хотя бы постарайтесь производить переделку в SDK поэтапно, чтобы можно было периодически сливать новые фичи в ветку разработки SDK. Легче сказать, чем сделать, но всё же попробуйте.
Скорее всего, вам потребуется переделать весь DI. Даже если вам кажется, что это не так, лучше заложите время с учётом, что всё же придется.
Дальнейшее развитие SDK
Мы провели основные работы по превращению приложения в SDK и его интеграции в другие приложения. Но у нас еще осталось много работы — рефакторинг слабых мест, уменьшение объема (размер целевых МП на Android вырос в полтора раза, а на IOS вообще в два), детальное логирование работы SDK, множество мелких правок. А там не за горами выпуск новых фич.
Вы дочитали до конца? Поздравляем! Надеюсь, наш опыт разработки и интеграции одного мобильного приложения в другое поможет кому-то еще и упростит такую нелегкую задачу.