Ask me anything
Ithy Logo
Sign in to save

Создайте Ваше Первое Масштабируемое Android-Приложение на Kotlin: Полное Руководство по Многомодульности!

Откройте для себя, как структурировать сложное приложение с помощью модулей, Jetpack Compose, Hilt, Retrofit и API Rick and Morty.

kotlin-multimodule-android-app-d7xroi33

Ключевые Моменты Этого Руководства

  • Мастерство Модульности: Узнайте, как многомодульная архитектура с разделением на 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.

mindmap root["Многомодульное Android приложение
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 клиента без реальных сетевых запросов.
Каждый модуль может иметь свою директорию `src/test` и `src/androidTest` для соответствующих тестов.


Рекомендуемые Запросы для Дальнейшего Изучения


Источники и Полезные Ссылки


Last updated May 17, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI