Как стать автором
Обновить
287.32
TINKOFF
IT’s Tinkoff — просто о сложном

Как сделать два приложения из одного. Опыт Тинькофф Джуниор

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

Привет, меня зовут Андрей и я занимаюсь приложениями Тинькофф и Тинькофф Джуниор для платформы Android. Хочу рассказать о том, как мы собираем два похожих приложения из одной кодовой базы.


Тинькофф Джуниор — это мобильное банковское приложение, ориентированное на детей до 14 лет. Оно похоже на обычное приложение для взрослых, только в него добавлены некоторые функции (например, темы оформления), а другие, наоборот, выключены (например, кредитки).


.


На старте проекта мы рассматривали различные варианты его реализации и приняли ряд решений. Сразу же стало очевидно, что два приложения (Тинькофф и Тинькофф Джуниор) будут иметь значительную часть общего кода. Мы не хотели делать форк от старого приложения, а потом копировать исправления ошибок и новый общий функционал. Чтобы работать с двумя приложениями сразу, мы рассматривали три варианта: Gradle Flavors, Git Submodules, Gradle Modules.


Gradle Flavors


Многие наши разработчики уже пробовали использовать Flavors, плюс мы могли применить многомерную сборку (multi-dimensional flavors) для использования с уже существующими flavors.
Однако Flavors имеют один фатальный недостаток. Android Studio считает кодом только код активного флейвора — то есть то, что лежит в папке main и в папке флейвора. Остальной код считается текстом наравне с комментариями. Это накладывает ограничения на некоторые инструменты студии: поиск использования кода, рефакторинг и другие.


Git Submodules


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


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


Многомодульная архитектура


Последний вариант — перейти на многомодульную архитектуру. Этот подход лишен недостатков, которые есть у двух других. Однако переход на многомодульную архитектуру требует временных затрат на рефакторинг.


На момент начала работы над Тинькофф Джуниор у нас было два модуля: маленький модуль API, описывающий работу с сервером, и большой монолитный модуль application, в котором была сосредоточена основная часть кода проекта.


drawingdrawing
В итоге мы хотели получить два модуля приложений: adult и junior и некий общий core-модуль. Мы выделили два варианта:


  • Вынесение общего кода в общий модуль common. Этот подход «правильнее», однако он требует больше времени. Мы оценивали объемы переиспользования кода приблизительно в 80%.
    drawing
  • Преобразование модуля приложения в библиотеку и подключение этой библиотеки к тонким модулям adult и junior. Этот вариант быстрее, однако он принесет в Тинькофф Джуниор код, который никогда не будет выполняться.
    drawing

У нас было время в запасе, и мы решили начать разработку по первому варианту (модуль common) с условием перейти к быстрому варианту, когда у нас закончится время на рефакторинг.
В итоге так и случилось: мы перенесли часть проекта в модуль common, а потом оставшийся модуль application превратили в библиотеку. В результате сейчас мы получили такую структуру проекта:


drawing

У нас есть модули с фичами, что позволяет нам разграничивать «взрослый», общий или «детский» код. Однако модуль application всё еще достаточно большой, и сейчас там хранится около половины проекта.


Превращаем приложение в библиотеку


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


  1. Открыть файл build.gradle модуля
  2. Удалить applicationId из конфигурации модуля
  3. В начале файла заменить apply plugin: 'com.android.application' на apply plugin: 'com.android.library'
  4. Сохранить изменения и синхронизировать проект в Android Studio (File > Sync Project with Gradle Files)

Однако конвертация заняла несколько дней и итоговый дифф получился таким:


  • 183 files changed
  • 1601 insertions(+)
  • 1920 deletions(-)

Что же пошло не так?

Прежде всего в библиотеках идентификаторы ресурсов — это не константы. В библиотеках, как и в приложениях, генерируется файл R.java со списком идентификаторов ресурсов. И в библиотеках значения идентификаторов не являются константными. Джава не позволяет делать свитч по неконстантным значениям, и все свитчи нужно заменить на if-else.


// Application
int id = view.getId();
switch(id) {
   case R.id.button1:
       action1();
       break;
   case R.id.button2:
       action2();
       break;
}

// Library
int id = view.getId();
if (id == R.id.button1) {
   action1();
} else if (id == R.id.button2) {
   action2();
}

Далее мы столкнулись с коллизией пакетов.
Предположим, у вас есть библиотека, у которой package = com.example, и от этой библиотеки зависит приложение с package = com.example.app. Тогда в библиотеке будет сгенерирован класс com.example.R, а в приложении, соответственно, com.example.app.R. Теперь создадим в приложении активити com.example.MainActivity, в которой попробуем обратиться к R-классу. Без явного импорта будет использован R-класс библиотеки, в котором не указаны ресурсы приложения, а только ресурсы библиотеки. Однако Android Studio не подсветит ошибку и при попытке перейти из кода к ресурсу всё будет окей.


Dagger


В качестве фреймворка для инъекции зависимостей мы используем Dagger.
В каждом модуле, содержащем активити, фрагменты и сервисы, у нас есть обычные интерфейсы, в которых описаны inject-методы для этих сущностей. В модулях приложений (adult и junor) интерфейсы-компоненты даггера наследуются от этих интерфейсов. В модулях мы приводим компоненты к необходимым для данного модуля интерфейсам.


Мультибиндинги


Разработку нашего проекта значительно упрощает использование мультибиндингов.
В одном из общих модулей мы определяем интерфейс. В каждом модуле приложения (adult, junior) описываем реализацию этого интерфейса. С помощью аннотации @Binds указываем даггеру, что всякий раз вместо интерфейса необходимо инжектить его конкретную реализацию для детского или взрослого приложения. Также мы нередко собираем коллекцию реализаций интерфейса (Set или Map), при этом такие реализации описаны в разных модулях приложения.


Флейворы


Для разных целей мы собираем несколько вариантов приложения. Флейворы, описанные в базовом модуле, должны быть описаны и в зависимых модулях. Также для корректной работы Android Studio необходимо, чтобы во всех модулях проекта были выбраны совместимые варианты сборки.


Выводы


За короткий срок мы реализовали новое приложение. Теперь мы отгружаем новый функционал в двух приложениях, написав его один раз.


При этом мы потратили некоторое время на рефакторинг, попутно уменьшив технический долг, и перешли на многомодульную архитектуру. По пути мы столкнулись с ограничениями со стороны Android SDK и Android Studio, с которыми успешно справились.

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

Публикации

Информация

Сайт
www.tinkoff.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия