Создайте Ваше Первое Масштабируемое Android-Приложение на Kotlin: Полное Руководство по Многомодульности!
Откройте для себя, как структурировать сложное приложение с помощью модулей, Jetpack Compose, Hilt, Retrofit и API Rick and Morty.
Ключевые Моменты Этого Руководства
- Мастерство Модульности: Узнайте, как многомодульная архитектура с разделением на
app,featureиcoreслои улучшает масштабируемость, тестируемость и поддержку ваших Android-проектов. - Современный Инструментарий Android: Освойте мощь Jetpack Compose для создания декларативного UI, Hilt для эффективного внедрения зависимостей, Retrofit для сетевых операций и Navigation-Compose для плавной навигации между экранами.
- Приключение с Rick & Morty: Создайте функциональное приложение, отображающее персонажей из популярного API Rick and Morty, демонстрируя практическое применение описанных технологий.
Введение в Многомодульную Разработку на Kotlin
В современном мире Android-разработки создание сложных и легко поддерживаемых приложений требует продуманного подхода к архитектуре. Многомодульность — это один из ключевых принципов, позволяющий разделить приложение на независимые, но взаимодействующие компоненты. В этом руководстве мы создадим простое, но показательное многомодульное приложение на Kotlin. Мы не будем использовать Kotlin Multiplatform (KMP), сосредоточившись на классической Android-разработке.
Наше приложение будет состоять из трёх основных модулей: app (точка входа), feature (пользовательский интерфейс и логика конкретной функциональности) и core (общие компоненты, такие как сетевое взаимодействие и модели данных). Мы будем использовать Jetpack Compose для построения UI, Navigation-Compose для навигации, Hilt для внедрения зависимостей (DI) и Retrofit для взаимодействия с API Rick and Morty (https://rickandmortyapi.com/api).
Пример графа зависимостей в многомодульном приложении.
Зачем Нужна Многомодульная Архитектура?
Многомодульный подход предлагает ряд существенных преимуществ, особенно для растущих проектов:
- Улучшенная Масштабируемость: Легче добавлять новые функции как отдельные, изолированные модули, не затрагивая остальную часть кодовой базы.
- Ускорение Сборки: Gradle может кэшировать результаты сборки неизмененных модулей, что значительно сокращает время компиляции проекта при инкрементальных изменениях.
- Улучшенная Тестируемость: Модули можно тестировать изолированно, что упрощает написание и поддержку unit- и интеграционных тестов.
- Повторное Использование Кода: Общие компоненты, вынесенные в
coreмодуль (например, сетевой слой, утилиты), могут быть легко переиспользованы в различных фичах или даже других проектах. - Четкое Разделение Ответственности: Каждый модуль имеет свою зону ответственности, что делает код более понятным, организованным и легким для навигации. Команды разработчиков могут работать над разными модулями параллельно с меньшим количеством конфликтов.
- Упрощение Рефакторинга: Изменения внутри одного модуля с меньшей вероятностью повлияют на другие части приложения, если интерфейсы взаимодействия четко определены.
Обзор Структуры Проекта
Наше приложение будет иметь следующую структуру модулей:
app: Главный модуль приложения, являющийся точкой входа. Он отвечает за инициализацию DI (Hilt), настройку основной навигации и сборку всех остальных модулей. Зависит от модуляfeature.feature: Модуль, содержащий конкретную функциональность — в нашем случае, отображение списка персонажей Rick and Morty и деталей о них. Включает UI (экраны на Jetpack Compose), ViewModels и логику, специфичную для этой фичи. Зависит от модуляcore.core: Библиотечный модуль, содержащий общие компоненты, используемые в других модулях. Сюда входят сетевой слой (Retrofit, интерфейсы API, модели данных), утилиты и базовые DI-конфигурации. Этот модуль не должен зависеть от других модулей типаfeatureилиapp.
Примерная структура директорий:
RickAndMortyApp/
├── app/ # Модуль приложения
│ ├── build.gradle.kts
│ └── src/main/...
├── feature/ # Модуль фичи (персонажи)
│ ├── build.gradle.kts
│ └── src/main/...
├── core/ # Основной модуль с общей логикой
│ ├── build.gradle.kts
│ └── src/main/...
├── build.gradle.kts # Скрипт сборки уровня проекта
└── settings.gradle.kts # Настройки проекта, подключение модулей
Ключевые Технологии в Нашем Стеке
Для создания нашего приложения мы будем использовать современный набор технологий, рекомендованный для Android-разработки:
- Kotlin: Официальный язык программирования для Android, известный своей лаконичностью, безопасностью и выразительностью.
- Jetpack Compose: Современный декларативный UI-тулкит от Google для создания нативных интерфейсов Android. Позволяет описывать UI как функции от состояния.
- Navigation-Compose: Компонент из Jetpack Navigation, предназначенный для удобной организации навигации между Composable-экранами.
- Retrofit: Типобезопасный HTTP-клиент для Android и Java от Square, упрощающий взаимодействие с REST API.
- Moshi: Современная библиотека для парсинга JSON в объекты Kotlin, хорошо интегрируется с Retrofit.
- Hilt: Библиотека для внедрения зависимостей (DI) от Google, построенная на Dagger. Упрощает использование DI в Android-приложениях, интегрируясь с компонентами Jetpack.
- Coroutines: Для асинхронного программирования, особенно при выполнении сетевых запросов.
- ViewModel: Компонент из Android Architecture Components для хранения и управления UI-связанными данными, устойчивый к изменениям конфигурации.
Ниже представлена диаграмма, отражающая оценку этих технологий по нескольким важным аспектам для разработки многомодульного Android-приложения. Оценки являются субъективными и основаны на общем опыте разработки.
Эта диаграмма помогает визуализировать, как каждая технология способствует различным аспектам разработки, таким как простота интеграции в модульную структуру, влияние на производительность приложения и повышение общей продуктивности команды разработчиков.
Пошаговое Руководство по Реализации
Шаг 1: Настройка Проекта и Модулей
Создание Модулей
1. Создайте новый проект Android Studio с пустой активностью Compose.
2. Добавьте новые модули:
core: File -> New -> New Module... -> Android Library. Назовите его "core".feature: Аналогично, создайте модуль "feature" как Android Library.
Конфигурация settings.gradle.kts
В файле settings.gradle.kts на уровне проекта убедитесь, что все модули включены:
// settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "RickAndMortyApp"
include(":app")
include(":core")
include(":feature")
Конфигурация build.gradle.kts Файлов
Ниже приведены ключевые зависимости и плагины для каждого модуля. Версии могут отличаться; используйте актуальные на момент разработки.
core/build.gradle.kts
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt") // Для Moshi codegen, если используется
}
android {
namespace = "com.example.core"
compileSdk = 34
defaultConfig {
minSdk = 24
// targetSdk = 34 // Не обязательно для library
}
// ...
}
dependencies {
// Retrofit & Moshi
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0") // Используйте актуальную версию
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0") // Для генерации адаптеров Moshi
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Hilt (если общие DI компоненты нужны здесь, обычно Hilt modules в app/feature)
implementation("com.google.dagger:hilt-android:2.48") // Используйте актуальную версию
kapt("com.google.dagger:hilt-compiler:2.48")
}
feature/build.gradle.kts
plugins {
id("com.android.library")
kotlin("android")
id("kotlin-kapt") // Hilt использует kapt
id("dagger.hilt.android.plugin") // Hilt Gradle Plugin
}
android {
namespace = "com.example.feature"
compileSdk = 34
defaultConfig {
minSdk = 24
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4" // Проверьте совместимость с вашей версией Kotlin/Compose
}
// ...
}
dependencies {
implementation(project(":core"))
// Compose
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.0")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.0")
// Navigation Compose
implementation("androidx.navigation:navigation-compose:2.7.7")
// ViewModel & LiveData (Lifecycle)
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Coil for image loading
implementation("io.coil-kt:coil-compose:2.5.0")
}
app/build.gradle.kts
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-kapt") // Hilt использует kapt
id("dagger.hilt.android.plugin") // Hilt Gradle Plugin
}
android {
namespace = "com.example.rickandmortyapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.rickandmortyapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
// ...
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4" // Согласуйте с feature модулем
}
// ...
}
dependencies {
implementation(project(":feature"))
implementation(project(":core")) // Часто непрямая зависимость через feature, но может быть и прямая
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.activity:activity-compose:1.8.2")
// Hilt (основные зависимости уже в feature, здесь только для Application класса)
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
// Compose (может быть уже транзитивно из feature)
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.2.0")
// ...
}
Примечание: Не забудьте добавить плагин Hilt в build.gradle.kts корневого проекта.
// build.gradle.kts (уровень проекта)
plugins {
// ...
id("com.google.dagger.hilt.android") version "2.48" apply false // Используйте актуальную версию
}
Шаг 2: Реализация Модуля core
Модели Данных
Создайте data class'ы для ответа API и объекта персонажа. Используйте аннотации Moshi для сериализации/десериализации.
core/src/main/java/com/example/core/api/CharacterModels.kt:
package com.example.core.api
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CharactersResponse(
@Json(name = "info") val info: Info,
@Json(name = "results") val results: List<Character>
)
@JsonClass(generateAdapter = true)
data class Character(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
@Json(name = "status") val status: String,
@Json(name = "species") val species: String,
@Json(name = "image") val image: String,
@Json(name = "origin") val origin: Origin,
@Json(name = "location") val location: Location
)
@JsonClass(generateAdapter = true)
data class Info(
@Json(name = "count") val count: Int,
@Json(name = "pages") val pages: Int,
@Json(name = "next") val next: String?,
@Json(name = "prev") val prev: String?
)
@JsonClass(generateAdapter = true)
data class Origin(
@Json(name = "name") val name: String,
@Json(name = "url") val url: String
)
@JsonClass(generateAdapter = true)
data class Location(
@Json(name = "name") val name: String,
@Json(name = "url") val url: String
)
Интерфейс Retrofit API
core/src/main/java/com/example/core/api/RickAndMortyApi.kt:
package com.example.core.api
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface RickAndMortyApi {
@GET("character")
suspend fun getCharacters(@Query("page") page: Int): CharactersResponse
@GET("character/{id}")
suspend fun getCharacterDetails(@Path("id") characterId: Int): Character
}
Hilt Модуль для Сетевых Зависимостей
Создайте Hilt модуль в core (или app, если зависимости предоставляются оттуда) для предоставления Retrofit и API сервиса.
core/src/main/java/com/example/core/di/NetworkModule.kt:
package com.example.core.di
import com.example.core.api.RickAndMortyApi
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
@Provides
@Singleton
fun provideRetrofit(moshi: Moshi): Retrofit {
return Retrofit.Builder()
.baseUrl("https://rickandmortyapi.com/api/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
@Provides
@Singleton
fun provideRickAndMortyApi(retrofit: Retrofit): RickAndMortyApi {
return retrofit.create(RickAndMortyApi::class.java)
}
}
Шаг 3: Реализация Модуля feature
ViewModel
feature/src/main/java/com/example/feature/characters/CharactersViewModel.kt:
package com.example.feature.characters
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.core.api.Character
import com.example.core.api.RickAndMortyApi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
sealed class CharactersUiState {
object Loading : CharactersUiState()
data class Success(val characters: List<Character>) : CharactersUiState()
data class Error(val message: String) : CharactersUiState()
}
sealed class CharacterDetailUiState {
object Loading : CharacterDetailUiState()
data class Success(val character: Character) : CharacterDetailUiState()
data class Error(val message: String) : CharacterDetailUiState()
}
@HiltViewModel
class CharactersViewModel @Inject constructor(
private val api: RickAndMortyApi
) : ViewModel() {
private val _charactersUiState = MutableStateFlow<CharactersUiState>(CharactersUiState.Loading)
val charactersUiState: StateFlow<CharactersUiState> = _charactersUiState
private val _characterDetailUiState = MutableStateFlow<CharacterDetailUiState>(CharacterDetailUiState.Loading)
val characterDetailUiState: StateFlow<CharacterDetailUiState> = _characterDetailUiState
fun fetchCharacters(page: Int = 1) {
viewModelScope.launch {
_charactersUiState.value = CharactersUiState.Loading
try {
val response = api.getCharacters(page)
_charactersUiState.value = CharactersUiState.Success(response.results)
} catch (e: Exception) {
_charactersUiState.value = CharactersUiState.Error(e.message ?: "Unknown error")
}
}
}
fun fetchCharacterDetails(characterId: Int) {
viewModelScope.launch {
_characterDetailUiState.value = CharacterDetailUiState.Loading
try {
val character = api.getCharacterDetails(characterId)
_characterDetailUiState.value = CharacterDetailUiState.Success(character)
} catch (e: Exception) {
_characterDetailUiState.value = CharacterDetailUiState.Error(e.message ?: "Unknown error")
}
}
}
}
Composable Экраны
feature/src/main/java/com/example/feature/characters/CharactersScreen.kt (список):
package com.example.feature.characters
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.rememberAsyncImagePainter
import com.example.core.api.Character
@Composable
fun CharactersListScreen(
viewModel: CharactersViewModel = hiltViewModel(),
onCharacterClick: (Int) -> Unit
) {
val uiState by viewModel.charactersUiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.fetchCharacters()
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Персонажи Rick & Morty") })
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) {
when (val state = uiState) {
is CharactersUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is CharactersUiState.Success -> {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.characters) { character ->
CharacterRow(character = character, onClick = {
onCharacterClick(character.id)
})
}
}
}
is CharactersUiState.Error -> {
Text(
text = "Ошибка: ${state.message}",
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
@Composable
fun CharacterRow(character: Character, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberAsyncImagePainter(character.image),
contentDescription = character.name,
modifier = Modifier.size(64.dp).padding(end = 8.dp),
contentScale = ContentScale.Crop
)
Column {
Text(text = character.name, style = MaterialTheme.typography.titleMedium)
Text(text = "Статус: ${character.status}", style = MaterialTheme.typography.bodySmall)
Text(text = "Вид: ${character.species}", style = MaterialTheme.typography.bodySmall)
}
}
}
}
feature/src/main/java/com/example/feature/characters/CharacterDetailScreen.kt (детали):
package com.example.feature.characters
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.rememberAsyncImagePainter
@Composable
fun CharacterDetailScreen(
characterId: Int,
viewModel: CharactersViewModel = hiltViewModel(),
onBackClick: () -> Unit
) {
val uiState by viewModel.characterDetailUiState.collectAsState()
LaunchedEffect(characterId) {
viewModel.fetchCharacterDetails(characterId)
}
Scaffold(
topBar = {
TopAppBar(
title = {
// Динамический заголовок после загрузки данных
val title = when (val state = uiState) {
is CharacterDetailUiState.Success -> state.character.name
else -> "Загрузка..."
}
Text(title)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Назад")
}
}
)
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) {
when (val state = uiState) {
is CharacterDetailUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is CharacterDetailUiState.Success -> {
val character = state.character
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = rememberAsyncImagePainter(character.image),
contentDescription = character.name,
modifier = Modifier.size(200.dp).padding(bottom = 16.dp),
contentScale = ContentScale.Crop
)
Text("Имя: ${character.name}", style = MaterialTheme.typography.headlineSmall)
Text("Статус: ${character.status}", style = MaterialTheme.typography.bodyLarge)
Text("Вид: ${character.species}", style = MaterialTheme.typography.bodyLarge)
Text("Происхождение: ${character.origin.name}", style = MaterialTheme.typography.bodyLarge)
Text("Местоположение: ${character.location.name}", style = MaterialTheme.typography.bodyLarge)
}
}
is CharacterDetailUiState.Error -> {
Text(
text = "Ошибка: ${state.message}",
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
Шаг 4: Реализация Модуля app
Application Класс с Hilt
app/src/main/java/com/example/rickandmortyapp/RickAndMortyApplication.kt:
package com.example.rickandmortyapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class RickAndMortyApplication : Application()
Не забудьте указать этот класс в AndroidManifest.xml:
<application
android:name=".RickAndMortyApplication"
... >
MainActivity и Навигация
app/src/main/java/com/example/rickandmortyapp/MainActivity.kt:
package com.example.rickandmortyapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.feature.characters.CharacterDetailScreen
import com.example.feature.characters.CharactersListScreen
import com.example.rickandmortyapp.ui.theme.RickAndMortyAppTheme // Создайте или используйте стандартную тему
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RickAndMortyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppNavigation()
}
}
}
}
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "characterList") {
composable("characterList") {
CharactersListScreen(
onCharacterClick = { characterId ->
navController.navigate("characterDetail/${'$'}{characterId}")
}
)
}
composable(
route = "characterDetail/{characterId}",
arguments = listOf(navArgument("characterId") { type = NavType.IntType })
) { backStackEntry ->
val characterId = backStackEntry.arguments?.getInt("characterId")
characterId?.let {
CharacterDetailScreen(
characterId = it,
onBackClick = { navController.popBackStack() }
)
}
}
}
}
Визуализация Архитектуры Приложения
Для лучшего понимания взаимодействия между модулями и их ключевыми компонентами, рассмотрим следующую ментальную карту. Она иллюстрирует зависимости и основные элементы каждого модуля в нашем приложении Rick & Morty.
Rick & Morty API"] id1["Модуль App :house:"] id1_1["MainActivity (Точка входа @AndroidEntryPoint)"] id1_2["AppNavigation (NavHost)"] id1_3["RickAndMortyApplication (@HiltAndroidApp)"] id1_4["Зависимости: feature, core"] id1_5["Тема приложения (Compose Theme)"] id2["Модуль Feature :sparkles:"] id2_1["UI Экраны (Jetpack Compose)"] id2_1_1["CharactersListScreen"] id2_1_2["CharacterDetailScreen"] id2_1_3["CharacterRow (Composable)"] id2_2["ViewModels (@HiltViewModel)"] id2_2_1["CharactersViewModel"] id2_3["Состояния UI (Sealed Classes)"] id2_3_1["CharactersUiState"] id2_3_2["CharacterDetailUiState"] id2_4["Зависимости: core, Hilt, Coil"] id3["Модуль Core :gear:"] id3_1["Сетевой слой (Retrofit & Moshi)"] id3_1_1["RickAndMortyApi (Интерфейс)"] id3_1_2["NetworkModule (Hilt @Module)"] id3_1_2_1["Предоставление Retrofit"] id3_1_2_2["Предоставление Moshi"] id3_1_2_3["Предоставление ApiService"] id3_2["Модели данных (Data Classes с @JsonClass)"] id3_2_1["Character"] id3_2_2["CharactersResponse"] id3_2_3["Info, Origin, Location"] id3_3["Coroutines для асинхронности"]
Эта карта показывает, как модуль app является точкой входа и управляет общей навигацией, модуль feature содержит всю UI-логику и ViewModels для отображения данных, а модуль core предоставляет данные через Retrofit и Hilt-модули для сетевых запросов и моделей.
Сводка Ключевых Конфигураций
Для быстрого обзора, вот таблица с ключевыми конфигурациями и компонентами, используемыми в нашем многомодульном приложении:
| Компонент/Технология | Модуль | Ключевая Конфигурация/Пример | Описание |
|---|---|---|---|
| Определение модулей | settings.gradle.kts |
include(":app", ":feature", ":core") |
Регистрация модулей в проекте. |
| Зависимость между модулями | app/build.gradle.kts |
implementation(project(":feature")) |
Модуль app зависит от feature. |
| Зависимость между модулями | feature/build.gradle.kts |
implementation(project(":core")) |
Модуль feature зависит от core. |
| Hilt (DI) - Плагин | build.gradle.kts (все модули, использующие Hilt) |
id("dagger.hilt.android.plugin") |
Подключение Gradle-плагина Hilt. |
| Hilt (DI) - Зависимости | build.gradle.kts (все модули, использующие Hilt) |
implementation("com.google.dagger:hilt-android:2.48") kapt("com.google.dagger:hilt-compiler:2.48") |
Подключение библиотек Hilt. |
| Hilt (Application) | app (Application Class) |
@HiltAndroidApp |
Инициализация Hilt на уровне приложения. |
| Hilt (Activity/Fragment/View) | app или feature (Activity) |
@AndroidEntryPoint |
Разрешение внедрения зависимостей в Android компоненты. |
| Hilt (ViewModel) | feature (ViewModel Class) |
@HiltViewModel class CharactersViewModel @Inject constructor(...) |
Внедрение зависимостей в ViewModel. |
| Retrofit & Moshi | core/build.gradle.kts |
implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-moshi:2.9.0") implementation("com.squareup.moshi:moshi-kotlin:1.15.0") |
Библиотеки для работы с сетью и парсинга JSON. |
| API Интерфейс | core |
interface RickAndMortyApi { @GET("character") suspend fun getCharacters(...): CharactersResponse } |
Определение эндпоинтов API с помощью аннотаций Retrofit. |
| Jetpack Compose | feature/build.gradle.kts & app/build.gradle.kts |
buildFeatures { compose = true } implementation("androidx.compose.ui:ui:1.6.0") |
Включение и подключение Compose для UI. |
| Navigation Compose | feature/build.gradle.kts & app/build.gradle.kts |
implementation("androidx.navigation:navigation-compose:2.7.7") |
Библиотека для организации навигации в Jetpack Compose. |
Видеоурок: Создание Многомодульного Android Приложения
Для дополнительного понимания процесса создания многомодульного Android-приложения, предлагаю ознакомиться со следующим видео. Хотя оно может отличаться в деталях реализации, оно отлично демонстрирует общие принципы модульности и работу с API, включая примеры с Rick and Morty API, что делает его весьма релевантным нашему руководству.
Просмотр этого видео поможет вам визуализировать некоторые из шагов, описанных выше, и увидеть, как различные компоненты собираются вместе в рабочее приложение.
Часто Задаваемые Вопросы (FAQ)
Почему Hilt, а не Koin, для внедрения зависимостей?
Обе библиотеки, Hilt и Koin, являются популярными решениями для внедрения зависимостей в Android. Hilt, построенный на Dagger, предлагает строгую проверку зависимостей на этапе компиляции, что помогает выявлять ошибки раньше. Он также тесно интегрирован с компонентами Jetpack и является рекомендуемым Google решением. Koin, с другой стороны, проще в настройке и использует рефлексию во время выполнения, что может быть менее производительным в некоторых случаях, но обеспечивает большую гибкость и меньшее количество boilerplate кода. Выбор часто зависит от предпочтений команды и требований проекта; для этого примера был выбран Hilt из-за его сильной типизации и интеграции с экосистемой Android.
Как обрабатывать состояния загрузки и ошибки в Jetpack Compose?
В Jetpack Compose состояния загрузки, успеха и ошибки обычно управляются с помощью `StateFlow` или `LiveData` во ViewModel и представляются с помощью sealed class'ов (как показано в примере `CharactersUiState`). В Composable функции вы подписываетесь на это состояние и в зависимости от его текущего значения отображаете соответствующий UI: индикатор загрузки, список данных или сообщение об ошибке. Использование `LaunchedEffect` позволяет запускать загрузку данных при первом отображении экрана или при изменении ключей.
Легко ли добавить новые функции в такую архитектуру?
Да, это одно из главных преимуществ многомодульной архитектуры. Новую функциональность можно реализовать в отдельном `feature` модуле. Этот модуль будет иметь свои экраны, ViewModels и будет зависеть от `core` модуля для доступа к общим ресурсам (например, API). Затем этот новый `feature` модуль подключается к `app` модулю, где настраивается навигация к его экранам. Такой подход минимизирует влияние на существующий код и упрощает параллельную разработку.
Какие распространенные проблемы могут возникнуть при настройке многомодульного проекта?
Основные сложности могут включать:
- Управление зависимостями Gradle: Синхронизация версий библиотек между модулями может быть сложной. Использование Gradle version catalogs или `buildSrc` может помочь.
- Циклические зависимости: Когда модуль А зависит от Б, а Б зависит от А. Это ошибка проектирования, которую нужно избегать, правильно разделяя ответственность.
- Время сборки: Хотя модульность ускоряет инкрементальные сборки, первоначальная настройка и очень большое количество мелких модулей могут замедлить полную сборку, если не оптимизировать конфигурацию Gradle.
- Сложность навигации между модулями: Требуется четкое определение интерфейсов или использование общего координатора навигации в `app` модуле.
- Конфигурация DI (Hilt/Dagger): Настройка DI в многомодульной среде требует внимательности, особенно с областями видимости компонентов и модулей.
Как тестировать отдельные модули в таком приложении?
Модульность значительно упрощает тестирование.
- Unit-тесты: Классы в `core` и `feature` модулях (например, ViewModels, репозитории, use-кейсы) можно тестировать изолированно с помощью JUnit и Mockito/MockK. Зависимости легко мокируются.
- Интеграционные тесты: Можно тестировать взаимодействие компонентов внутри одного модуля, например, ViewModel с репозиторием.
- UI-тесты (Compose): Экраны в `feature` модулях можно тестировать с помощью `androidx.compose.ui.test`. Hilt предоставляет утилиты для тестирования компонентов, использующих DI.
- Тестирование API в `core` модуле: Можно использовать MockWebServer для тестирования Retrofit клиента без реальных сетевых запросов.
Рекомендуемые Запросы для Дальнейшего Изучения
- Как реализовать пагинацию данных с Retrofit и Jetpack Compose в многомодульном приложении?
- Какие лучшие практики для управления зависимостями Gradle в крупных многомодульных Android-проектах?
- Как написать Unit и UI тесты для Jetpack Compose и ViewModel с Hilt?
- В чем разница между модулями :api и :impl в многомодульной архитектуре Android?
Источники и Полезные Ссылки
Last updated May 17, 2025