Chat
Ask me anything
Ithy Logo

Создание современного Android-приложения: Многомодульность, Clean Architecture и Jetpack Compose

Детальное руководство по архитектуре и реализации мобильного приложения на Kotlin с Rick and Morty API.

android-multimodule-clean-architecture-compose-6himvxoq
Привет! Как коллега сеньор-разработчик, я подготовил для вас подробное руководство по созданию многомодульного Android-приложения на Kotlin с использованием запрошенного стека технологий. Мы рассмотрим структуру, ключевые аспекты реализации и лучшие практики.

Ключевые моменты

  • Многомодульная архитектура: Использование app, feature (с разделением на presentation, domain, data) и core модулей для улучшения масштабируемости, тестируемости и изоляции кода.
  • Чистая Архитектура (Clean Architecture): Четкое разделение ответственностей между слоями для создания независимой от UI и источников данных бизнес-логики.
  • Современный стек технологий: Применение Jetpack Compose для UI, Hilt для DI, Retrofit для сети, Room для локальной базы данных и DataStore для настроек пользователя, обеспечивая эффективную и поддерживаемую разработку.

Структура многомодульного приложения

Многомодульная архитектура является краеугольным камнем для создания крупных и легко поддерживаемых Android-приложений. Она позволяет разделить проект на независимые, но взаимодействующие компоненты (модули Gradle). Для нашего приложения Rick and Morty мы применим следующую структуру:

Пример графа зависимостей модулей

Пример графа зависимостей в многомодульном приложении.

App-модуль (`:app`)

Это главный модуль приложения, служащий точкой входа. Его основные задачи:

  • Инициализация приложения (например, Hilt через класс `Application`).
  • Настройка корневой навигации (Bottom Navigation Bar с тремя вкладками).
  • Сборка финального APK.
  • Зависит от всех `feature`-модулей, но не наоборот, для обеспечения слабой связанности. Он объединяет все части приложения.

Feature-модули (`:feature:*`)

Каждый `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-модуля.

Core/Common-модули (Общие модули)

Эти модули содержат код, который может быть переиспользован в нескольких `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: Может содержать контракты для межмодульной навигации, если требуется более сложная координация.

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


Визуализация архитектуры приложения

Для лучшего понимания взаимосвязей между модулями и их компонентами, рассмотрим следующую ментальную карту:

mindmap root["Архитектура приложения Rick & Morty"] id_app["app (Точка входа, Навигация)"] id_features["Feature-модули"] id_characters["feature_characters"] id_char_pres["Presentation (Compose, ViewModel)"] id_char_dom["Domain (UseCases, Repo Interfaces)"] id_char_data["Data (Retrofit, RepositoryImpl)"] id_favorites["feature_favorites"] id_fav_pres["Presentation"] id_fav_dom["Domain"] id_fav_data["Data (Room, RepositoryImpl)"] id_profile["feature_profile"] id_prof_pres["Presentation"] id_prof_dom["Domain"] id_prof_data["Data (DataStore, RepositoryImpl)"] id_char_detail["feature_character_detail"] id_detail_pres["Presentation"] id_detail_dom["Domain"] id_detail_data["Data (Использует Character Repo)"] id_core["Core/Common-модули (Общие)"] id_core_network["core_network (Настройка Retrofit)"] id_core_database["core_database (Настройка Room)"] id_core_ui["core_common_ui (Общие UI компоненты)"] id_core_common["core_common (Утилиты)"]

Эта карта иллюстрирует, как `app` модуль зависит от `feature`-модулей, которые, в свою очередь, имеют внутреннюю слоистую структуру и могут использовать общие `core`-модули.


Ключевые технологии и паттерны

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

  • Язык программирования: Kotlin – официальный язык для Android-разработки, предлагающий современный синтаксис и безопасность.
  • Пользовательский интерфейс: Jetpack Compose – декларативный UI-фреймворк для создания нативных интерфейсов с меньшим количеством кода.
  • Архитектура: Clean Architecture – для разделения ответственностей и создания тестируемого и масштабируемого кода.
  • Внедрение зависимостей (DI): Hilt – библиотека от Google, упрощающая DI в Android-приложениях.
  • Работа с сетью: Retrofit – для выполнения HTTP-запросов к The Rick and Morty API.
  • Локальное хранение (избранное): Room Persistence Library – для создания и управления локальной SQLite базой данных.
  • Локальное хранение (профиль): Jetpack DataStore – для асинхронного хранения пар ключ-значение или типизированных объектов.
  • Навигация: Jetpack Navigation Compose – для управления переходами между экранами в Compose-приложении.
  • Асинхронность: Kotlin Coroutines и Flow – для управления фоновыми задачами и потоками данных.
  • Пагинация: Paging 3 Library (рекомендуется для списка всех персонажей) – для эффективной загрузки и отображения больших списков данных.

Детальное руководство по реализации

1. Настройка проекта и зависимостей

Создайте новый проект в 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`-модуле.

2. Реализация Чистой Архитектуры в Feature-модулях

Рассмотрим на примере `feature_characters`.

Data Layer (`:feature_characters:data`)

Здесь реализуются интерфейсы репозиториев. Для работы с 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.

Domain Layer (`:feature_characters:domain`)

Содержит бизнес-логику.

Интерфейс репозитория:


// 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)
    }
}
    

Presentation Layer (`:feature_characters:presentation`)

Содержит 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}")
}
    

3. Внедрение зависимостей с Hilt

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()
    }
}
    

4. Навигация с Jetpack Compose Navigation

В `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-разработке.


Часто задаваемые вопросы (FAQ)

Зачем нужна многомодульность в Android-приложении?
Многомодульность улучшает масштабируемость проекта, позволяя командам работать над разными частями приложения независимо. Она сокращает время сборки, так как пересобираются только измененные модули. Также повышается изоляция кода, что упрощает тестирование и поддержку, и способствует лучшей организации проекта.
В чем основные преимущества Чистой Архитектуры?
Чистая Архитектура (Clean Architecture) делает бизнес-логику приложения независимой от UI, базы данных и фреймворков. Это повышает тестируемость (domain слой можно тестировать как чистый Kotlin/Java код), гибкость (легче заменять компоненты data или presentation слоев) и поддерживаемость кода. Основной принцип – зависимость должна идти от внешних слоев к внутренним.
Как Hilt помогает в многомодульном проекте?
Hilt упрощает внедрение зависимостей в многомодульных проектах, предоставляя стандартный способ управления зависимостями. Он интегрируется с Jetpack компонентами, такими как ViewModel. В многомодульной среде Hilt позволяет каждому модулю определять свои зависимости и предоставлять их другим модулям, соблюдая при этом принципы инкапсуляции. Это уменьшает количество шаблонного кода для DI.
Как передавать данные между экранами в Jetpack Compose Navigation?
Данные можно передавать через маршруты навигации. При определении `composable` в `NavHost`, вы можете указать аргументы в строке маршрута (например, `"profile/{userId}"`). Затем, при навигации, вы передаете фактическое значение (`navController.navigate("profile/123")`). В целевом `Composable` эти аргументы можно извлечь из `NavBackStackEntry`. Для сложных объектов рекомендуется передавать только ID, а полные данные загружать в ViewModel целевого экрана.
Почему для списка всех персонажей рекомендуется использовать Paging 3?
Rick and Morty API может возвращать большое количество персонажей. Библиотека Paging 3 позволяет загружать и отображать данные постранично, по мере необходимости. Это снижает нагрузку на сеть, уменьшает потребление памяти и улучшает производительность UI, так как приложению не нужно загружать и хранить весь список персонажей сразу.

Заключение

Предложенная многомодульная структура с использованием Чистой Архитектуры, Jetpack Compose, Hilt и других современных технологий позволяет создать гибкое, масштабируемое и легко поддерживаемое Android-приложение. Четкое разделение на слои и модули упрощает разработку, тестирование и командную работу, что особенно важно для проектов любого размера. Следование этим принципам поможет вам, как сеньор-разработчику, создавать качественные и надежные мобильные приложения.


Рекомендуемые запросы


Результаты поиска

codingwithmitch.com
CodingWithMitch.com
rickandmortyapi.com
Documentation
Ask Ithy AI
Download Article
Delete Article