|
| 1 | +--- |
| 2 | +sidebar_position: 25 |
| 3 | +--- |
| 4 | + |
| 5 | +# Legacy подходы, которые можно встретить в старых проектах ICEROCK |
| 6 | + |
| 7 | +## buildSrc |
| 8 | + |
| 9 | +:::caution |
| 10 | + |
| 11 | +Директория упразднена! |
| 12 | + |
| 13 | +На новых проектах вместо `buildSrc` используется `build-logic`. |
| 14 | + |
| 15 | +::: |
| 16 | + |
| 17 | +`buildSrc` - [специальная директория Gradle](https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:build_sources). Она предназначена для реализации логики сборки, не привязанной к конкретному gradle модулю. По сути это исходники библиотеки, которая автоматически будет подгружена в gradle и все классы, объявленные в этой библиотеке, будут доступны в любом месте Gradle конфигурации (в `build.gradle.kts`). |
| 18 | + |
| 19 | +В этой директории можно увидеть собственный `build.gradle.kts` и исходный код библиотеки. `build.gradle.kts` определяет, как будет собираться данная библиотека и какие зависимости ей требуются. Исходный код библиотеки в нашем проекте содержит только один объект `Deps`, содержащий константы и зависимости, необходимые нашему проекту. |
| 20 | + |
| 21 | +В `build.gradle.kts` можно увидеть подключение нескольких зависимостей: |
| 22 | + |
| 23 | +```kotlin |
| 24 | +dependencies { |
| 25 | + implementation("dev.icerock:mobile-multiplatform:0.9.2") |
| 26 | + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32") |
| 27 | + implementation("com.android.tools.build:gradle:4.1.3") |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +В `Deps.kt` объявлен объект `Deps` с перечислением версий зависимостей и самих зависимостей в виде констант, для удобного обращения к ним в gradle конфигурации. |
| 32 | + |
| 33 | +Ниже приведено пояснение ко всем составляющим данного объекта: |
| 34 | + |
| 35 | +```kotlin |
| 36 | +object Deps { |
| 37 | + // сначала перечисляются константами версии каждой из использующихся зависимостей, |
| 38 | + // они приватные, так как используются данные константы только внутри самого Deps |
| 39 | + private const val materialVersion = "1.2.1" |
| 40 | + // ... |
| 41 | + |
| 42 | + // во вложенном объекте Android описываются версии используемых android sdk |
| 43 | + object Android { |
| 44 | + const val compileSdk = 30 |
| 45 | + const val targetSdk = 30 |
| 46 | + const val minSdk = 21 |
| 47 | + } |
| 48 | + |
| 49 | + // во вложенном объекте Plugins содержатся объявления каждого используемого нами gradle плагина, |
| 50 | + // в специальном контейнере GradlePlugin (данный класс приходит от зависимости mobile-multiplatform-gradle-plugin) |
| 51 | + // https://github.com/icerockdev/mobile-multiplatform-gradle-plugin/blob/master/src/main/kotlin/GradlePlugin.kt#L10 |
| 52 | + object Plugins { |
| 53 | + // мы можем объявить gradle плагин, используя только id, в таком случае будет недоступно |
| 54 | + // подключение артефакта с данным плагином (для этого нужно указание свойства module), |
| 55 | + // такой подход используется для плагинов, которые подключаются с gradlePluginPortal либо |
| 56 | + // содержатся в зависимостях, которые указаны в buildSrc/build.gradle.kts |
| 57 | + val androidApplication = GradlePlugin(id = "com.android.application") |
| 58 | + // для плагинов, которые требуют подключения дополнительных артефактов, нужно указать свойство |
| 59 | + // module и в build.gradle.kts подключить в buildscript.dependencies |
| 60 | + val kotlinSerialization = GradlePlugin( |
| 61 | + id = "org.jetbrains.kotlin.plugin.serialization", |
| 62 | + module = "org.jetbrains.kotlin:kotlin-serialization:$kotlinxSerializationPluginVersion" |
| 63 | + ) |
| 64 | + // ... |
| 65 | + } |
| 66 | + |
| 67 | + // вложенный объект Libs содержит объявления каждой внешней библиотеки |
| 68 | + object Libs { |
| 69 | + // в Android содержатся библиотеки, относящиеся только к android, |
| 70 | + // они используются в android-app либо как зависимости androidMain sourceSet'а |
| 71 | + object Android { |
| 72 | + // объявление зависимости происходит в виде обычной строки |
| 73 | + const val appCompat = "androidx.appcompat:appcompat:$androidAppCompatVersion" |
| 74 | + // ... |
| 75 | + |
| 76 | + // в Tests указываются зависимости для тестов в android-app либо androidTest sourceSet'а |
| 77 | + object Tests { |
| 78 | + // объявление также происходит в виде обычной строки |
| 79 | + const val espressoCore = "androidx.test.espresso:espresso-core:$espressoCoreVersion" |
| 80 | + // ... |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + // в MultiPlatform объекте содержатся объявления всех мультиплатформенных библиотек, |
| 85 | + // они используется в mpp-library и вложенных в нее модулях |
| 86 | + object MultiPlatform { |
| 87 | + // те зависимости, которые не предполагается использовать в Kotlin/Native export, |
| 88 | + // указываются в виде простой строки. Указывается артефакт метаданных, по которому |
| 89 | + // gradle найдет все артефакты для нужных таргетов |
| 90 | + const val kotlinSerialization = |
| 91 | + "org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion" |
| 92 | + // ... |
| 93 | + |
| 94 | + // те зависимости, которые будут экспортироваться в iOS framework, объявляются в виде |
| 95 | + // MultiPlatformLibrary контейнера (из mobile-multiplatform-gradle-plugin) |
| 96 | + // https://github.com/icerockdev/mobile-multiplatform-gradle-plugin/blob/master/src/main/kotlin/MultiPlatformLibrary.kt#L10 |
| 97 | + // для простоты чаще всего используется преобразование из строки с путем до артефакта |
| 98 | + // метаданных, через метод defaultMPL, с указанием в аргументах тех таргетов, которые |
| 99 | + // нужно автоматически заполнить (заполнение идет согласно именованию стандартной |
| 100 | + // публикации мультиплатформенных библиотек) |
| 101 | + val mokoResources = "dev.icerock.moko:resources:$mokoResourcesVersion" |
| 102 | + .defaultMPL(ios = true) |
| 103 | + // ... |
| 104 | + |
| 105 | + // в Tests указываются зависимости для тестов в mpp-library и вложенных модулях |
| 106 | + object Tests { |
| 107 | + // зависимости указываются в виде строк |
| 108 | + const val kotlinTest = "org.jetbrains.kotlin:kotlin-test-common:$kotlinTestVersion" |
| 109 | + // ... |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + // в Detekt указываются специфичные для detekt плагина зависимости |
| 114 | + object Detekt { |
| 115 | + // зависимости указываются в виде строк |
| 116 | + const val detektFormatting = "io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion" |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + // в Modules указываются gradle модули нашего проекта |
| 121 | + object Modules { |
| 122 | + object Feature { |
| 123 | + // объявляются модули в виде контейнера MultiPlatformModule (из mobile-multiplatform-gradle-plugin) |
| 124 | + // https://github.com/icerockdev/mobile-multiplatform-gradle-plugin/blob/master/src/main/kotlin/MultiPlatformModule.kt#L10 |
| 125 | + // в name указывается путь до gradle проекта, а в exported - флаг, нужно ли экспортировать |
| 126 | + // классы данного модуля в iOS framework |
| 127 | + val auth = MultiPlatformModule( |
| 128 | + name = ":mpp-library:feature:auth", |
| 129 | + exported = true |
| 130 | + ) |
| 131 | + } |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +Данный класс определяет константы, которые мы будем использовать в `build.gradle.kts` всех gradle модулей, что сокращает вероятность ошибки и позволяет менять версии/пути до библиотек в одном месте. |
| 137 | + |
| 138 | +## Shared & Domain Factory |
| 139 | + |
| 140 | +> Чтобы понять, что такое *Shared* и *Domain* Factory, нужно будет немного разобраться с подходом разделения ответственности и проброса необходимых зависимостей в проектах. |
| 141 | +
|
| 142 | +### Как было раньше? |
| 143 | +У нас были такие модули как *domain* и *shared*, а также фабрики *DomainFactory* и *SharedFactory*. |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | +Модуль *domain* включал в себя описания доменных сущностей, описание классов для работы с сервером, логику преобразования серверных ответов в те самые доменные сущности, с которыми могло работать приложение. Также в нём содержалась и доменная фабрика *DomainFactory*, которая создавала классы для работы с сетью, репозитории, управляющие данными, и производила настройку http-клиента. А также именно расширениями к *DomainFactory* реализовывались создания всех остальных фабрик для фичей. |
| 148 | + |
| 149 | + |
| 150 | + |
| 151 | +Модуль *shared* содержал большое количество полезных расширений, вспомогательных методов, упрощений и прочих переиспользуемых между модулями вещей. |
| 152 | + |
| 153 | +А внутри модуля *mpp-library* располагалась и *SharedFactory* (либо просто *Factory*, на разных старых проектах название может быть разным). Её предназначение было получить с натива все данные, необходимые для реализации *DomainFactory*, и, соответственно, *DomainFactory* на основе этих же данных могла реализовывать свои внутренние компоненты. Плюс *mpp-library* служила прослойкой для маппинга всех доменных сущностей в сущности фичей. Например, модель юзера могла быть и в модуле авторизации, и в модуле профилей. Но auth:User и profile:User - это были разные модели, и преобразование от доменной сущности domain:User (которую мы получали после преобразования ответа сервера) требовалось для каждой из них. |
| 154 | + |
| 155 | + |
| 156 | + |
| 157 | +И чтобы в фичах мы могли спокойно кидать запросы, использовать модели данных и применять вспомогательные методы из *shared*, приходилось добавлять практически во всех фичах зависимости на *shared*. |
| 158 | + |
| 159 | +> По началу всё шло неплохо. Производительность не сильно страдала. Проблемы начались тогда, когда мы имеем уже объёмный проект, состоящий из 10-15 модулей с фичами. И в какой-то момент нам для одной из фичей надо в *shared* добавить небольшой код или поправить реализацию уже имеющегося. Это приводило к тому, что на iOS начинают пересобираться абсолютно все зависящие на shared модули, а разработчик, поменявший одну строчку, мог ждать сборки iOS по 10 с лишним минут. |
| 160 | +
|
| 161 | +> И вторая большая проблема заключалась в том, что мы вынуждены были плодить множество сущностей. На примере всё того же юзера у нас была сетевая сущность юзера, которую присылал бэк, доменная сущность юзера, в которую мы преобразовывали сетевую, а далее для фичей авторизации и профиля — ещё по одной сущности, которые относятся уже к самим фичам, а они, в свою очередь, должны преобразовываться из доменных. Самый банальный пример — если на сервере добавляют новое поле в сущности, которое нам нужно использовать, то его приходилось пробрасывать через все эти круги ада и тратить время. |
| 162 | + |
| 163 | +### К чему мы пришли? |
| 164 | + |
| 165 | +Из-за всего вышесказанного, от данного подхода было принято отказываться в сторону более нового - с учётом независимости фичи и проброса в неё внешних зависимостей. |
| 166 | + |
| 167 | +> Если в рамках работы над проектом вам встречается модуль *domain*, *shared* или *DomainFactory*, то это проект, построенный на старом варианте архитектуры. |
| 168 | +> |
| 169 | +> Этот подход не актуален! |
| 170 | +> |
| 171 | +> В рамках поддержки существующих фичей, при невозможности изменения добавления зависимости от модуля на проброс зависимостей через интерфейсы, придётся использовать старый способ. |
| 172 | +> При создании же новых фичей, даже с учётом старой архитектуры в этом проекте, их нужно реализовывать актуальным способом. |
| 173 | +
|
| 174 | +**Что изменилось:** |
| 175 | + |
| 176 | +- Модуль *domain* упразднён! |
| 177 | + |
| 178 | + Сущность и модели у каждой фичи свои, в рамках модуля этой фичи. И они содержат достаточный набор данных для её работы. Но в случаях, когда одни и те же сущности должны использоваться между несколькими фичами, для того, чтобы избежать дублирования, такие сущности выносятся в отдельный модуль и в зависимость добавляется именно он. |
| 179 | + |
| 180 | +- *DomainFactory* и SharedFactory упразднены. |
| 181 | + |
| 182 | + Вместо них мы напрямую через koin получаем зависимости. |
| 183 | + |
| 184 | +- Модуль *shared* упразднён. |
| 185 | + |
| 186 | + Подобные общие компоненты реализовываются внутри модуля *mpp-library*. |
| 187 | + |
| 188 | +- Для реализации логики работы с данными, либо с сервером используются репозитории. Каждая ViewModel описывает у себя интерфейс репозитория, покрывающий её нужды. Либо бывают случаи общего интерфейса репозитория на несколько ViewModel, но в рамках одной фичи. Реализация этого репозитория должна передаваться при создании ViewModel. Сами реализации создаются в модуле *mpp-library*, а он, как мы знаем, имеет информацию и о view-моделях (т.к. *mpp-library* знает о других модулях) и о сетевом слое. Соответственно в рамках него без проблем можно описать реализации этих интерфейсов и пробросить их в фабрику фичи, которая, в свою очередь, передаст реализацию во ViewModel. Это же помогает избежать пачки мапперов из сетевой сущности в доменную, из доменной в фичёвую. |
0 commit comments