Привет друзья! Я уже довольно долго занимаюсь разработкой под Android и должен признать, что современные приложения отличаются от приложений десятилетней давности в первую очередь более четкой архитектурой и разбитием на модули. Современным де-факто стандартом является многомодульность, ну или хотя бы наличие несколько Gradle модулей на проект. Как правило в проекте есть основной модуль app, несколько фиче-модулей, а также core/domain/data модули. 

Эти модули связаны по вполне стандартной схеме: 

Когда у нас есть одно приложение - проблем никаких нет, все модули существуют тесно друг с другом и нам не слишком важно чтобы фичи были не связаны друг с другом (хотя да, это может привести к множеству проблем таким как: )

На самом деле, ничто не мешает иметь нам несколько приложений в одном проекте. Для этого просто добавим еще один :app gradle-модуль, объявим в нем плагин android-application, и вуаля - Android Studio показывает нам еще один вариант запуска. 

Например, мы разрабатываем два приложения: одно для любителей котиков CatsApp а другое для поклонников пёсиков DogsApp, при этом весь функционал у нас дублируется (есть экран для показа списка котиков или собачек, а также экран для показа детальной информации) а отличаются только основные модули, в которых например могут быть объявлены разные network-конфигурации, файлы с темами themes.xml и по разному инициализируется DI-графы. Пример такого проекта можно увидеть здесь.

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

Второй вариант 

это когда у нас есть два независимо развивающиеся приложения в рамках одной компании и бизнес требует объединить части функционала или переиспользовать функционал одного приложения в другом. 

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

Как подружить модули из разных проектов? Ну во первых мы можем просто настроить видимость одним проектом модулей из другого, просто скачав их в соседние директории и обновив settings.gradle основного проекта включив в него модули другого. Для этого нам нужно прописать путь к необходимым модулям:

project(':feature-list').projectDir = new File('../app_to_be_reused/feature-list')

include ':feature-details'

project(':feature-details').projectDir = new File('../app_to_be_reused/feature-details')

include ':domain' 

project(':domain').projectDir = new File('../app_to_be_reused/domain')

К сожалению, если нам нужен только модуль :feature-list, нам также придется добавить зависимости и того модуля в наш проект, то есть подтянуть :domain и :feature-details.

Не подготовив изначально модули переиспользуемого проекта, у нас возникнет множество проблем с тем, чтобы просто заимпортировать всё что нужно и синхронизировать основной проект. Например если в вашем переиспользуемом модуле :feature-1 есть Activity, то они не будут ничего знать о визуальных темах основного проекта :cats-app .

Также появляются проблемы с синхронизацией правильных веток в разных git проектах, велика опасность того что в переиспользуемом проекте поменялась gradle конфигурация, и мы всегда должны следить что работаем с актуальной версией проекта. Это в свою очередь вызывает проблемы с CI билдами, которые теперь должен выкачивать два проекта вместо одного и собирать и тестировать оба проекта на одном пайплайне. 

Немного облегчает работу Git сабмодули (подмодули) Они позволяют включать в наш репозиторий - другой репозиторий и держать ссылку на правильную версию (tag) проекта с зависимостями. Подробнее про сабмодули можно прочитать можно почитать тут. Схема зависимостей в этом случае будет такая же как и в предыдущем примере

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

Третий вариант 

основан на публикации части приложения в отдельную android-библиотеку, чтобы другое приложение могло её подключить и использовать как любую другую стороннюю SDK. Если приложение с зависимостями достаточно хорошо модуляризовано, то можно попробовать экспортировать нужные модули, добавив для них точки входа из другого приложения. Как правило, мы используем gradle плагин maven-publish для публикации чистых java/kotlin библиотек, но с недавнего времени, его поддержка появилась в Android Gradle Plugin и мы можем экспортировать андроидные модули а не только java-библиотеки

Рассмотрим на примере. Наше приложение DogsApp состоит из модулей app, feature-list, feature-details и domain. feature-list отвечает за вывод списка собачек которые поступают нам из app модуля. feature-details отвечает за вывод информации и изображения отдельной собачки. domain содержит в себе общую информацию, которая переиспользуется во всех остальных модулях. 

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

Для этого воспользуемся плагином maven-publish и добавим следующий код в feature-details.build.gradle:

android {
   ...
   publishing {
       singleVariant('release')
   }
}

afterEvaluate {
   publishing {
       publications {
           release(MavenPublication) {
               from components.release
           }
       }
   }
}

singleVariant указывает на то что мы хотим опубликовать только один билд-вариант. Чтобы можно было использовать и дебажную и релизную версию нашей библиотеки нам нужно исользовать allVariants() и components.default:

 ...
   publishing {
      multipleVariants {
         allVariants()
      }
   }
}
afterEvaluate {
   publishing {
       publications {
           allVariants(MavenPublication) {
               from components.default
           }
       }
   }
}

Теперь мы можем опубликовать наш модуль командой gradle :feature-details:publishToMavenLocal. Чтобы можно было использовать эту зависимость, давайте добавим в приложение CatsApp локальный maven-репозиторий:

settings.gradle:

dependencyResolutionManagement {
   repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
   repositories {
       google()
       mavenCentral()
       mavenLocal()
   }
}

И теперь в cats_app/build.gradle мы можем добавить зависимость на экспортированную библиотеку:

dependencies {

   ...

   implementation 'com.example:feature-details:1.0.0'

}

Однако когда мы попробуем собрать CatsApp у нас появятся ошибки сборки, потому что билд-система не может найти модуль :domain. Получается нам надо опубликовать и его тоже. На самом деле нам пришлось бы каскадно опубликовать все зависимости, которые использует модуль :feature-details. Для этого уберите код, который мы до этого добавили для публикации :feature-details и добавьте в корневой build.gradle следующее

subprojects {
   apply plugin: "maven-publish"
   afterEvaluate {
       if (!plugins.hasPlugin("android")) {
           if (plugins.hasPlugin("android-library")) {
               android {
                   publishing {
                       multipleVariants {
                           allVariants()
                       }
                   }
               }
           }
           publishing {
               publications {
                   allVariants(MavenPublication) {
                       afterEvaluate {
                           if (plugins.hasPlugin("java")) {
                               from components.java
                           } else if (plugins.hasPlugin("android-library")) {
                               from components.default
                           }
                       }
                   }
               }
           }
       }
   }

Subprojects говорит нам о том что мы хотим применить наш код ко всем подмодулям. На самом деле мы не хотим чтобы был опубликован основой модуль приложения :dogs-app поэтому мы отфильтруем его по наличию плагина android: !plugins.hasPlugin("android"). Также нужно отметить что модуль :domain - это java библиотека, и у него нет билд-вариантов, поэтому мы не должны добавлять ему это информацию, а добавим билд варианты только для android-библиотек: 

 if (plugins.hasPlugin("android-library")) {
     android {

Также, поскольку у нас есть и java и android-модули, нам придется различать как мы хотим их публиковать. Если к модулю применен плагин java то он будет публиковать как java-библиотека, а если плагин android-library то мы будем публиковать набор .aar архивов с соответствующими билд-вариантами.

if (plugins.hasPlugin("java")) {
    from components.java
} else if (plugins.hasPlugin("android-library")) {
    from components.default
}

Теперь когда мы опубликуем нашу библиотеку той же командой gradle :feature-details:publishToMavenLocal все зависимости окажутся в нашем локальном maven-репозитории и клиентское приложение CatsApp соберется без проблем. Полный код можно посмотреть здесь

А приходилось ли вам объединять разные приложения или переиспользовать код другой команды? Поделитесь в комментариях подходами которые используются в вашей компании.

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


  1. Rusrst
    27.07.2023 10:08

    С публикацией библиотек есть нюансы, исходников не будет (или как минимум туда не вынесешь изменения) и если вдруг вы поймаете баг, то искать его то ещё удовольствие...

    П.с. я не уверен (у нас чуть другой подход), но чтобы не публиковать другие модули должны подойти compileOnly/debugOnly/api.


  1. chugunovr Автор
    27.07.2023 10:08

    @Rusrstтам можно добавлять исходники и javadocs и тогда это решит проблему дебаггинга

    withSourcesJar()

    withJavadocJar()

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