app, feature (с разделением на presentation, domain, data) и core модулей для улучшения масштабируемости, тестируемости и изоляции кода.Многомодульная архитектура является краеугольным камнем для создания крупных и легко поддерживаемых Android-приложений. Она позволяет разделить проект на независимые, но взаимодействующие компоненты (модули Gradle). Для нашего приложения Rick and Morty мы применим следующую структуру:
Пример графа зависимостей в многомодульном приложении.
Это главный модуль приложения, служащий точкой входа. Его основные задачи:
Каждый `feature`-модуль инкапсулирует определенную функциональность приложения. В нашем случае это:
:feature_characters: Отображение списка всех персонажей.:feature_favorites: Отображение списка избранных персонажей.:feature_profile: Управление профилем пользователя.:feature_character_detail: Отображение подробной информации о персонаже.Каждый `feature`-модуль строго следует принципам Чистой Архитектуры и внутренне разделен на три подмодуля (или логических слоя в рамках одного Gradle-модуля):
:presentation (Слой представления)Отвечает за все, что связано с UI. Содержит Jetpack Compose `Composable`-функции, `ViewModel` для управления состоянием UI и обработки пользовательского ввода, а также навигацию внутри фичи. Этот слой зависит от `domain`-слоя.
:domain (Слой бизнес-логики)Ядро фичи. Это чистый Kotlin-модуль, не имеющий зависимостей от Android SDK. Содержит бизнес-логику, `UseCase` (интеракторы), интерфейсы репозиториев и доменные модели (POJO/data class). Не зависит от других слоев, что обеспечивает его тестируемость и переносимость.
:data (Слой данных)Отвечает за предоставление данных для `domain`-слоя. Содержит реализации интерфейсов репозиториев, определенных в `domain`-слое. Взаимодействует с источниками данных: сетевые запросы (Retrofit к Rick and Morty API), локальная база данных (Room для избранных персонажей) и хранилище настроек (DataStore для профиля пользователя). Зависит от `domain`-слоя (для реализации его интерфейсов и использования моделей).
Схематичное изображение слоев Чистой Архитектуры внутри feature-модуля.
Эти модули содержат код, который может быть переиспользован в нескольких `feature`-модулях или на разных слоях. Примеры таких модулей:
:core_network: Настройка Retrofit, OkHttp клиента, общие Data Transfer Objects (DTO), если они используются в нескольких фичах.:core_database: Настройка Room Database, общие DAO или сущности, если применимо.:core_common_ui: Общие компоненты Jetpack Compose, темы, утилиты для UI.:core_common: Утилиты, расширения Kotlin, константы, базовые классы, не относящиеся напрямую к UI или данным.:core_navigation: Может содержать контракты для межмодульной навигации, если требуется более сложная координация.Такая структура способствует лучшей организации кода, ускоряет время сборки (за счет компиляции только измененных модулей) и упрощает командную работу.
Для лучшего понимания взаимосвязей между модулями и их компонентами, рассмотрим следующую ментальную карту:
Эта карта иллюстрирует, как `app` модуль зависит от `feature`-модулей, которые, в свою очередь, имеют внутреннюю слоистую структуру и могут использовать общие `core`-модули.
Выбор правильного стека технологий и архитектурных паттернов является залогом успешной разработки. В данном проекте мы будем использовать:
Создайте новый проект в Android Studio, выбрав шаблон "Empty Compose Activity". Далее сконфигурируйте `settings.gradle` для включения всех модулей (`app`, `feature_*`, `core_*`). В `build.gradle` каждого модуля добавьте необходимые зависимости. Например, для `app` модуля:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
// ... другие плагины
}
android {
// ... конфигурация android
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3" // Укажите актуальную версию
}
}
dependencies {
implementation(project(":feature_characters"))
implementation(project(":feature_favorites"))
implementation(project(":feature_profile"))
implementation(project(":feature_character_detail"))
// Зависимости для core модулей, если они есть
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform("androidx.compose:compose-bom:2024.05.00")) // Bill of Materials
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
// Navigation Compose
implementation("androidx.navigation:navigation-compose:2.7.7")
// Hilt
implementation("com.google.dagger:hilt-android:2.51.1")
// kapt("com.google.dagger:hilt-compiler:2.51.1") // или ksp
// ... другие зависимости (Retrofit, Room, DataStore в соответствующих data или core модулях)
}
Не забудьте аннотировать класс `Application` с помощью `@HiltAndroidApp` в `app`-модуле.
Рассмотрим на примере `feature_characters`.
Здесь реализуются интерфейсы репозиториев. Для работы с API Rick and Morty используется Retrofit.
Retrofit API Interface (например, в `core_network` или `feature_characters:data`):
// RickAndMortyApiService.kt
interface RickAndMortyApiService {
@GET("character")
suspend fun getAllCharacters(@Query("page") page: Int): CharacterListResponse
@GET("character/{id}")
suspend fun getCharacterById(@Path("id") characterId: Int): CharacterApiModel
}
// CharacterListResponse и CharacterApiModel - это ваши DTO
Реализация репозитория:
// CharacterRepositoryImpl.kt (в feature_characters:data)
class CharacterRepositoryImpl @Inject constructor(
private val apiService: RickAndMortyApiService,
private val characterDomainMapper: CharacterDomainMapper // Маппер из DTO в Domain модель
) : CharacterRepository { // CharacterRepository - интерфейс из domain слоя
override suspend fun getAllCharacters(page: Int): Result<List<Character>> {
return try {
val response = apiService.getAllCharacters(page)
Result.success(response.results.map { characterDomainMapper.mapToDomain(it) })
} catch (e: Exception) {
Result.failure(e)
}
}
// ... другие методы
}
Аналогично, в `feature_favorites:data` будет реализация `FavoritesRepository` с использованием Room, а в `feature_profile:data` – `ProfileRepository` с DataStore.
Содержит бизнес-логику.
Интерфейс репозитория:
// CharacterRepository.kt
interface CharacterRepository {
suspend fun getAllCharacters(page: Int): Result<List<Character>>
suspend fun getCharacterById(id: Int): Result<Character>
}
// Character - это ваша Domain модель
UseCase:
// GetCharactersUseCase.kt
class GetCharactersUseCase @Inject constructor(
private val repository: CharacterRepository
) {
suspend operator fun invoke(page: Int): Result<List<Character>> {
return repository.getAllCharacters(page)
}
}
Содержит UI и логику его отображения.
ViewModel:
// CharactersViewModel.kt
@HiltViewModel
class CharactersViewModel @Inject constructor(
private val getCharactersUseCase: GetCharactersUseCase
) : ViewModel() {
private val _charactersState = MutableStateFlow<List<Character>>(emptyList())
val charactersState: StateFlow<List<Character>> = _charactersState.asStateFlow()
// ... логика загрузки, обработки ошибок, состояния UI
fun loadCharacters(page: Int) {
viewModelScope.launch {
getCharactersUseCase(page)
.onSuccess { _charactersState.value = it }
.onFailure { /* обработка ошибки */ }
}
}
}
Composable экран:
// CharactersScreen.kt
@Composable
fun CharactersScreen(
navController: NavController,
viewModel: CharactersViewModel = hiltViewModel()
) {
val characters = viewModel.charactersState.collectAsState().value
// Используйте LazyColumn для отображения списка characters
// При клике на элемент: navController.navigate("character_detail_route/\${character.id}")
}
Hilt упрощает предоставление зависимостей. Аннотируйте конструкторы с `@Inject`, создавайте Hilt-модули (`@Module`, `@Provides`) для интерфейсов, билдеров (Retrofit, Room) и внешних библиотек. ViewModel'и аннотируются `@HiltViewModel`.
// AppModule.kt (например, в core_network или соответствующем data-модуле)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRickAndMortyApiService(retrofit: Retrofit): RickAndMortyApiService {
return retrofit.create(RickAndMortyApiService::class.java)
}
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://rickandmortyapi.com/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
В `app`-модуле настраивается `NavHost` и `BottomNavigationBar`.
// MainAppNavigation.kt (в app модуле)
@Composable
fun MainAppNavigation() {
val navController = rememberNavController()
Scaffold(
bottomBar = { AppBottomNavigationBar(navController) }
) { paddingValues ->
NavHost(
navController = navController,
startDestination = "characters_route", // Маршрут к feature_characters
modifier = Modifier.padding(paddingValues)
) {
composable("characters_route") {
// CharactersScreen() из feature_characters:presentation
}
composable("favorites_route") {
// FavoritesScreen() из feature_favorites:presentation
}
composable("profile_route") {
// ProfileScreen() из feature_profile:presentation
}
composable(
route = "character_detail_route/{characterId}",
arguments = listOf(navArgument("characterId") { type = NavType.IntType })
) { backStackEntry ->
val characterId = backStackEntry.arguments?.getInt("characterId")
// CharacterDetailScreen(characterId) из feature_character_detail:presentation
}
}
}
}
@Composable
fun AppBottomNavigationBar(navController: NavController) {
val items = listOf(
// Объекты, описывающие вкладки: маршрут, иконка, название
// ...
)
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(screen.title) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
Применение многомодульности и Чистой Архитектуры влияет на различные аспекты разработки. Диаграмма ниже представляет субъективную оценку этих влияний для ключевых компонентов системы. Оценка производится по шкале от 1 (минимальное проявление) до 5 (максимальное проявление).
Эта диаграмма помогает визуализировать, как различные части архитектуры способствуют общей гибкости и качеству проекта. Например, `Domain` слой обычно показывает высокие значения по тестируемости и изоляции кода.
В следующей таблице представлен сводный обзор основных модулей/слоев, используемых технологий и их ключевых обязанностей в рамках описываемого приложения:
| Модуль/Слой | Основные технологии | Ключевая ответственность | Зависимости |
|---|---|---|---|
app |
Hilt, Jetpack Navigation Compose, Android SDK | Точка входа, корневая навигация (BottomNav), сборка APK, инициализация DI, объединение feature-модулей. | Зависит от всех feature модулей и core модулей. |
feature/*:presentation |
Jetpack Compose, ViewModel, Flow/StateFlow, Hilt, Coroutines | Пользовательский интерфейс (UI), отображение данных, обработка пользовательского ввода, управление состоянием UI. | Зависит от feature/*:domain своего модуля. Может зависеть от core_common_ui. |
feature/*:domain |
Kotlin, Coroutines | Бизнес-логика, UseCases (интеракторы), интерфейсы репозиториев, доменные модели. Не зависит от Android SDK. | Не зависит от других слоев (чистый Kotlin). Является ядром фичи. |
feature/*:data |
Retrofit (для API), Room (для БД), DataStore (для настроек), Hilt, Coroutines, Flow, Kotlin | Реализация интерфейсов репозиториев, взаимодействие с источниками данных (сеть, локальная БД, DataStore), маппинг данных. | Зависит от feature/*:domain (для реализации интерфейсов). Может зависеть от core_network, core_database. |
core_network |
Retrofit, OkHttp, Gson/Moshi, Hilt, Kotlin | Настройка и предоставление сетевого клиента (Retrofit), общие DTO, обработчики ошибок сети. | Используется data-слоями feature-модулей. |
core_database |
Room, Hilt, Kotlin | Настройка и предоставление экземпляра Room Database, общие DAO (если есть), миграции. | Используется data-слоями feature-модулей. |
core_common_ui |
Jetpack Compose, Kotlin | Общие переиспользуемые UI компоненты (кнопки, индикаторы загрузки), темы, ресурсы. | Используется presentation-слоями feature-модулей и app-модулем. |
core_common |
Kotlin | Общие утилиты, расширения, константы, базовые классы, не связанные с конкретной технологией. | Используется любыми другими модулями по необходимости. |
Для более глубокого понимания принципов многомодульной архитектуры и Clean Architecture, рекомендую ознакомиться со следующим видео. В нем рассматриваются подходы к структурированию проекта, которые перекликаются с обсуждаемой нами темой:
Это видео поможет визуализировать концепции и увидеть практические примеры применения многомодульности в Android-разработке.
Предложенная многомодульная структура с использованием Чистой Архитектуры, Jetpack Compose, Hilt и других современных технологий позволяет создать гибкое, масштабируемое и легко поддерживаемое Android-приложение. Четкое разделение на слои и модули упрощает разработку, тестирование и командную работу, что особенно важно для проектов любого размера. Следование этим принципам поможет вам, как сеньор-разработчику, создавать качественные и надежные мобильные приложения.