Привет друзья! Я уже довольно долго занимаюсь разработкой под 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)
chugunovr Автор
27.07.2023 10:08@Rusrstтам можно добавлять исходники и javadocs и тогда это решит проблему дебаггинга
withSourcesJar()
withJavadocJar()
compileOnly подходит только для момента компиляции, а в рантайме публикуемый код закрашится потому что не сможет найти класс который остался в неопубликованном модуле
Rusrst
С публикацией библиотек есть нюансы, исходников не будет (или как минимум туда не вынесешь изменения) и если вдруг вы поймаете баг, то искать его то ещё удовольствие...
П.с. я не уверен (у нас чуть другой подход), но чтобы не публиковать другие модули должны подойти compileOnly/debugOnly/api.