company_banner

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

    Привет, меня зовут Андрей и я занимаюсь приложениями Тинькофф и Тинькофф Джуниор для платформы 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, с которыми успешно справились.

    Tinkoff.ru
    120,76
    IT’s Tinkoff.ru — просто о сложном
    Поделиться публикацией

    Похожие публикации

    Комментарии 11

      0
      Для чего отдельно модуль commonFeature и application(Library)? Может их стоило бы объединить?
        0

        У нас есть несколько модулей junior feature, несколько модулей common feature и несколько модулей adult feature. В каждом модуле описана 1 фича.
        Модуль application(library) — это остатки нашего монолита, который мы продолжаем разносить на фиче-модули.

          0
          Мы сейчас похожим начали заниматься… И ещё вопрос, вот модули junior feature и adult feature находятся рядом в корневой папке вместе с остальными общими модулями или каждый из этих модулей находятся внутри «модулей» приложение junior и adult? Не совсем понимаю позволяет ли студия так делать?
          И ещё вопросы:
          1) могут ли быть у junior и adult свои собственные flavors/build variants?
          2) Как в рамках CI происходит сборка разных приложений?
            0
            Все модули равноценны на уровне проекта, «модулей внутри модулей» нет.
            Сами модули можно размещать в разных папках и подпапках проекта (с указанием пути в settings.gradle)
              0
              вот модули junior feature и adult feature находятся рядом в корневой папке вместе с остальными общими модулями или каждый из этих модулей находятся внутри «модулей» приложение junior и adult?

              В "корневой" папке проекта можно создать дерево папок и хранить модули там.
              Примерно так
              root
              — application (легаси — монолит)
              — adult (модуль)
              — junior (модуль)
              — sources
              — — features
              — — — feature-A (модуль фичи А)
              — — — feature-B (модуль фичи Б)
              — — — feature-C (модуль фичи С)


              могут ли быть у junior и adult свои собственные flavors/build variants?

              Да, могут.


              Как в рамках CI происходит сборка разных приложений?

              Все просто, запускаем отдельно две таски)
              :adult:assembleDebug :junior:assembleDebug
              В данном случае таски имеют одинаковое имя, так что можно просто assembleDebug

                0
                В данном случае таски имеют одинаковое имя, так что можно просто assembleDebug

                Соберутся сразу два приложения, верно?
                  0

                  Конечно да, а, к примеру, таска installDebug установит дебажные версии двух приложений.

                    0
                    Круто.
                    А как с версионностью? Сквозная, одна на оба приложения?
                    Как-то помечаете коммиты в гите, когда изменения затрагивают отдельные приложения/когда сразу оба/когда только общий код?
                      +1

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


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

                      Спецпометок нет, но для двух приложений у нас отдельные проекты в трекере задач и по названию ветки видно "приложение-инициатор" изменений.

                        0
                        Ок. Спасибо за ответы!
          0
          Прошу прощения за оффтоп, а где-то можно обсудить функциональность Тинькофф Джуниор? Очень хочется попросить простую, как мне кажется, для реализации фичу.)

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

          Самое читаемое