Legacy подходы, которые можно встретить в старых проектах ICEROCK
buildSrc
caution
Директория упразднена!
На новых проектах вместо buildSrc используется build-logic.
buildSrc - специальная директория Gradle. Она предназначена для реализации логики сборки, не привязанной к конкретному gradle модулю. По сути это исходники библиотеки, которая автоматически будет подгружена в gradle и все классы, объявленные в этой библиотеке, будут доступны в любом месте Gradle конфигурации (в build.gradle.kts).
В этой директории можно увидеть собственный build.gradle.kts и исходный код библиотеки. build.gradle.kts определяет, как будет собираться данная библиотека и какие зависимости ей требуются. Исходный код библиотеки в нашем проекте содержит только один объект Deps, содержащий константы и зависимости, необходимые нашему проекту.
В build.gradle.kts можно увидеть подключение нескольких зависимостей:
dependencies {
implementation("dev.icerock:mobile-multiplatform:0.9.2")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32")
implementation("com.android.tools.build:gradle:4.1.3")
}
В Deps.kt объявлен объект Deps с перечислением версий зависимостей и самих зависимостей в виде констант, для удобного обращения к ним в gradle конфигурации.
Ниже приведено пояснение ко всем составляющим данного объекта:
object Deps {
// сначала перечисляются константами версии каждой из использующихся зависимостей,
// они приватные, так как используются данные константы только внутри самого Deps
private const val materialVersion = "1.2.1"
// ...
// во вложенном объекте Android описываются версии используемых android sdk
object Android {
const val compileSdk = 30
const val targetSdk = 30
const val minSdk = 21
}
// во вложенном объекте Plugins содержатся объявления каждого используемого нами gradle плагина,
// в специальном контейнере GradlePlugin (данный класс приходит от зависимости mobile-multiplatform-gradle-plugin)
// https://github.com/icerockdev/mobile-multiplatform-gradle-plugin/blob/master/src/main/kotlin/GradlePlugin.kt#L10
object Plugins {
// мы можем объявить gradle плагин, используя только id, в таком случае будет недоступно
// подключение артефакта с данным плагином (для этого нужно указание свойства module),
// такой подход используется для плагинов, которые подключаются с gradlePluginPortal либо
// содержатся в зависимостях, которые указаны в buildSrc/build.gradle.kts
val androidApplication = GradlePlugin(id = "com.android.application")
// для плагинов, которые требуют подключения дополнительных артефактов, нужно указать свойство
// module и в build.gradle.kts подключить в buildscript.dependencies
val kotlinSerialization = GradlePlugin(
id = "org.jetbrains.kotlin.plugin.serialization",
module = "org.jetbrains.kotlin:kotlin-serialization:$kotlinxSerializationPluginVersion"
)
// ...
}
// вложенный объект Libs содержит объявления каждой внешней библиотеки
object Libs {
// в Android содержатся библиотеки, относящиеся только к android,
// они используются в android-app либо как зависимости androidMain sourceSet'а
object Android {
// объявление зависимости происходит в виде обычной строки
const val appCompat = "androidx.appcompat:appcompat:$androidAppCompatVersion"
// ...
// в Tests указываются зависимости для тестов в android-app либо androidTest sourceSet'а
object Tests {
// объявление также происходит в виде обычной строки
const val espressoCore = "androidx.test.espresso:espresso-core:$espressoCoreVersion"
// ...
}
}
// в MultiPlatform объекте содержатся объявления всех мультиплатформенных библиотек,
// они используется в mpp-library и вложенных в нее модулях
object MultiPlatform {
// те зависимости, которые не предполагается использовать в Kotlin/Native export,
// указываются в виде простой строки. Указывается артефакт метаданных, по которому
// gradle найдет все артефакты для нужных таргетов
const val kotlinSerialization =
"org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion"
// ...
// те зависимости, которые будут экспортироваться в iOS framework, объявляются в виде
// MultiPlatformLibrary контейнера (из mobile-multiplatform-gradle-plugin)
// https://github.com/icerockdev/mobile-multiplatform-gradle-plugin/blob/master/src/main/kotlin/MultiPlatformLibrary.kt#L10
// для простоты чаще всего используется преобразование из строки с путем до артефакта
// метаданных, через метод defaultMPL, с указанием в аргументах тех таргетов, которые
// нужно автоматически заполнить (заполнение идет согласно именованию стандартной
// публикации мультиплатформенных библиотек)
val mokoResources = "dev.icerock.moko:resources:$mokoResourcesVersion"
.defaultMPL(ios = true)
// ...
// в Tests указываются зависимости для тестов в mpp-library и вложенных модулях
object Tests {
// зависимости указываются в виде строк
const val kotlinTest = "org.jetbrains.kotlin:kotlin-test-common:$kotlinTestVersion"
// ...
}
}
// в Detekt указываются специфичные для detekt плагина зависимости
object Detekt {
// зависимости указываются в виде строк
const val detektFormatting = "io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion"
}
}
// в Modules указываются gradle модули нашего проекта
object Modules {
object Feature {
// объявляются модули в виде контейнера MultiPlatformModule (из mobile-multiplatform-gradle-plugin)
// https://github.com/icerockdev/mobile-multiplatform-gradle-plugin/blob/master/src/main/kotlin/MultiPlatformModule.kt#L10
// в name указывается путь до gradle проекта, а в exported - флаг, нужно ли экспортировать
// классы данного модуля в iOS framework
val auth = MultiPlatformModule(
name = ":mpp-library:feature:auth",
exported = true
)
}
}
}
Данный класс определяет константы, которые мы будем использовать в build.gradle.kts всех gradle модулей, что сокращает вероятность ошибки и позволяет менять версии/пути до библиотек в одном месте.
Shared & Domain Factory
Чтобы понять, что такое Shared и Domain Factory, нужно будет немного разобраться с подходом разделения ответственности и проброса необходимых зависимостей в проектах.
Как было раньше?
У нас были такие модули как domain и shared, а также фабрики DomainFactory и SharedFactory.

Модуль domain включал в себя описания доменных сущностей, описание классов для работы с сервером, логику преобразования серверных ответов в те самые доменные сущности, с которыми могло работать приложение. Также в нём содержалась и доменная фабрика DomainFactory, которая создавала классы для работы с сетью, репозитории, управляющие данными, и производила настройку http-клиента. А также именно расширениями к DomainFactory реализовывались создания всех остальных фабрик для фичей.

Модуль shared содержал большое количество полезных расширений, вспомогательных методов, упрощений и прочих переиспользуемых между модулями вещей.
А внутри модуля mpp-library располагалась и SharedFactory (либо просто Factory, на разных старых проектах название может быть разным). Её предназначение было получить с натива все данные, необходимые для реализации DomainFactory, и, соответственно, DomainFactory на основе этих же данных могла реализовывать свои внутренние компоненты. Плюс mpp-library служила прослойкой для маппинга всех доменных сущностей в сущности фичей. Например, модель юзера могла быть и в модуле авторизации, и в модуле профилей. Но auth:User и profile:User - это были разные модели, и преобразование от доменной сущности domain:User (которую мы получали после преобразования ответа сервера) требовалось для каждой из них.

И чтобы в фичах мы могли спокойно кидать запросы, использовать модели данных и применять вспомогательные методы из shared, приходилось добавлять практически во всех фичах зависимости на shared.
По началу всё шло неплохо. Производительность не сильно страдала. Проблемы начались тогда, когда мы имеем уже объёмный проект, состоящий из 10-15 модулей с фичами. И в какой-то момент нам для одной из фичей надо в shared добавить небольшой код или поправить реализацию уже имеющегося. Это приводило к тому, что на iOS начинают пересобираться абсолютно все зависящие на shared модули, а разработчик, поменявший одну строчку, мог ждать сборки iOS по 10 с лишним минут.
И вторая большая проблема заключалась в том, что мы вынуждены были плодить множество сущностей. На примере всё того же юзера у нас была сетевая сущность юзера, которую присылал бэк, доменная сущность юзера, в которую мы преобразовывали сетевую, а далее для фичей авторизации и профиля — ещё по одной сущности, которые относятся уже к самим фичам, а они, в свою очередь, должны преобразовываться из доменных. Самый банальный пример — если на сервере добавляют новое поле в сущности, которое нам нужно использовать, то его приходилось пробрасывать через все эти круги ада и тратить время.
К чему мы пришли?
Из-за всего вышесказанного, от данного подхода было принято отказываться в сторону более нового - с учётом независимости фичи и проброса в неё внешних зависимостей.
Если в рамках работы над проектом вам встречается модуль domain, shared или DomainFactory, то это проект, построенный на старом варианте архитектуры.
Этот подход не актуален!
В рамках поддержки существующих фичей, при невозможности изменения добавления зависимости от модуля на проброс зависимостей через интерфейсы, придётся использовать старый способ. При создании же новых фичей, даже с учётом старой архитектуры в этом проекте, их нужно реализовывать актуальным способом.
Что изменилось:
Модуль domain упразднён!
Сущность и модели у каждой фичи свои, в рамках модуля этой фичи. И они содержат достаточный набор данных для её работы. Но в случаях, когда одни и те же сущности должны использоваться между несколькими фичами, для того, чтобы избежать дублирования, такие сущности выносятся в отдельный модуль и в зависимость добавляется именно он.
DomainFactory и SharedFactory упразднены.
Вместо них мы напрямую через koin получаем зависимости.
Модуль shared упразднён.
Подобные общие компоненты реализовываются внутри модуля mpp-library.
Для реализации логики работы с данными, либо с сервером используются репозитории. Каждая ViewModel описывает у себя интерфейс репозитория, покрывающий её нужды. Либо бывают случаи общего интерфейса репозитория на несколько ViewModel, но в рамках одной фичи. Реализация этого репозитория должна передаваться при создании ViewModel. Сами реализации создаются в модуле mpp-library, а он, как мы знаем, имеет информацию и о view-моделях (т.к. mpp-library знает о других модулях) и о сетевом слое. Соответственно в рамках него без проблем можно описать реализации этих интерфейсов и пробросить их в фабрику фичи, которая, в свою очередь, передаст реализацию во ViewModel. Это же помогает избежать пачки мапперов из сетевой сущности в доменную, из доменной в фичёвую.