Создаем Современное Android-Приложение: Многомодульность, Compose, Hilt, Retrofit и Room
Пошаговое руководство по разработке масштабируемого Kotlin-приложения с чистой архитектурой и новейшими технологиями.
Привет! В этом руководстве мы создадим простое, но функциональное многомодульное Android-приложение на Kotlin. Мы будем использовать самые актуальные и рекомендуемые технологии: Jetpack Compose для декларативного UI, Hilt для внедрения зависимостей, Retrofit для сетевых запросов и Room для локального хранения данных. Приложение будет иметь три основных экрана, переключаемых с помощью нижнего навигационного бара (Bottom Navigation Bar), а также возможность навигации на дополнительные экраны.
Ключевые Особенности Проекта
- Многомодульная Архитектура: Разделение приложения на логические модули для улучшения масштабируемости, поддерживаемости и ускорения сборки.
- Современный Стек Технологий: Использование Jetpack Compose, Hilt, Retrofit, Room и Kotlin Coroutines для создания эффективного и отзывчивого приложения.
- Четкое Разделение Ответственности: Реализация чистой архитектуры с разделением на слои данных (data), домена (domain) и представления (UI/feature).
- Интуитивная Навигация: Нижний навигационный бар для быстрого доступа к основным разделам и возможность перехода на детализированные экраны.
Понимание Многомодульной Архитектуры
Зачем нужна многомодульность?
Многомодульная архитектура предлагает ряд существенных преимуществ для разработки Android-приложений:
- Ускорение времени сборки: Gradle может кэшировать неизмененные модули и собирать только те, которые были изменены, что особенно заметно в крупных проектах.
- Улучшенное разделение ответственности (Separation of Concerns): Каждый модуль имеет свою четко определенную зону ответственности, что делает код более организованным и понятным.
- Повышенная поддерживаемость и масштабируемость: Легче вносить изменения или добавлять новую функциональность в изолированный модуль, не затрагивая остальную часть приложения.
- Возможность повторного использования кода: Общие модули (например, для работы с данными или UI-компонентами) могут быть переиспользованы в различных частях приложения или даже в других проектах.
- Параллельная работа команды: Разные команды или разработчики могут независимо работать над разными модулями.
- Улучшенная тестируемость: Модули можно тестировать изолированно.
Структура Нашего Приложения
Для нашего примера мы выберем следующую модульную структуру, которая обеспечивает хороший баланс между разделением ответственности и простотой для демонстрационных целей:
:app: Основной модуль приложения. СодержитMainActivity, классApplication, настройку навигации (NavHost и BottomNavigationBar), и объединяет все остальные модули.:core_data: Модуль для всей логики, связанной с данными. Включает реализацию Retrofit для сетевых запросов, Room для работы с локальной базой данных, репозитории и модели данных (DTO, Entities). Также здесь будут Hilt-модули для предоставления зависимостей этого слоя.:core_domain: Модуль для бизнес-логики. Содержит сценарии использования (UseCases) и доменные модели (чистые Kotlin-классы, не зависящие от Android Framework).:core_common_ui: Модуль для общих элементов пользовательского интерфейса, таких как кастомные компоненты Compose, темы, ресурсы и утилиты UI, которые могут использоваться несколькими feature-модулями.:feature_home: Модуль для функциональности главного экрана. Содержит Composable-функции для экрана "Домой" и его ViewModel.:feature_profile: Модуль для функциональности экрана профиля. Содержит Composable-функции для экрана "Профиль" и его ViewModel.:feature_settings: Модуль для функциональности экрана настроек. Содержит Composable-функции для экрана "Настройки" и его ViewModel.
Эта структура позволяет четко разделить логику представления, бизнес-логику и логику доступа к данным.
Точка входа
Навигация
Сборка UI"] idA2[":core_data
Retrofit (сеть)
Room (локальная БД)
Репозитории"] idA3[":core_domain
UseCases
Бизнес-логика
Доменные модели"] idA4[":core_common_ui
Общие UI компоненты
Темы
Ресурсы"] idB["Модули Функциональности (Features)"] idB1[":feature_home
Экран 'Домой'
(UI + ViewModel)"] idB2[":feature_profile
Экран 'Профиль'
(UI + ViewModel)"] idB3[":feature_settings
Экран 'Настройки'
(UI + ViewModel)"] idC["Ключевые Технологии"] idC1["Kotlin"] idC2["Jetpack Compose (UI)"] idC3["Hilt (DI)"] idC4["Retrofit (Сеть)"] idC5["Room (БД)"] idC6["Navigation Compose"] idC7["Kotlin Coroutines"] idD["Преимущества Архитектуры"] idD1["Масштабируемость"] idD2["Поддерживаемость"] idD3["Ускорение инкрементальной сборки"] idD4["Четкое разделение ответственностей"] idD5["Переиспользование кода"] idD6["Упрощенное тестирование"]
Приведенная выше диаграмма иллюстрирует взаимосвязи между модулями и ключевые технологии, используемые в нашем проекте.
Технологический Стек
Мы будем использовать следующие современные технологии и библиотеки:
Пример архитектуры Android-приложения, иллюстрирующий взаимодействие компонентов.
Kotlin
Основной язык программирования, выбранный за его краткость, безопасность и поддержку со стороны Google.
Jetpack Compose
Современный декларативный UI-тулкит для создания нативных пользовательских интерфейсов Android. Позволяет описывать UI с помощью Kotlin-функций.
Navigation Compose
Компонент для управления навигацией между экранами в приложениях на Jetpack Compose.
Hilt
Библиотека для внедрения зависимостей, построенная на базе Dagger. Упрощает интеграцию DI в Android-приложения.
Retrofit
Типобезопасный HTTP-клиент для Android и Java. Используется для взаимодействия с REST API.
Room
Библиотека персистентности, предоставляющая абстракцию над SQLite для удобной работы с локальной базой данных.
Kotlin Coroutines
Для управления асинхронными операциями, такими как сетевые запросы или обращения к базе данных, обеспечивая отзывчивость UI.
Настройка Проекта
Конфигурация Gradle
Сначала необходимо настроить файлы Gradle для поддержки многомодульности и подключения всех необходимых зависимостей.
Корневой `settings.gradle.kts`
В этом файле мы объявляем все модули нашего проекта:
// settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyMultiModuleApp"
include(":app")
include(":core_data")
include(":core_domain")
include(":core_common_ui")
include(":feature_home")
include(":feature_profile")
include(":feature_settings")
Корневой `build.gradle.kts` (Project level)
Здесь мы определяем версии плагинов и общие конфигурации:
// build.gradle.kts (Project level)
plugins {
id("com.android.application") version "8.2.0" apply false // Версия может отличаться
id("org.jetbrains.kotlin.android") version "1.9.20" apply false // Версия может отличаться
id("com.android.library") version "8.2.0" apply false
id("com.google.dagger.hilt.android") version "2.48.1" apply false // Актуальная версия Hilt
id("org.jetbrains.kotlin.kapt") version "1.9.20" apply false
}
Модульный `build.gradle.kts` (Пример для `:app` и `:core_data`)
Каждый модуль будет иметь свой `build.gradle.kts` файл с зависимостями.
Пример для :app модуля:
// :app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}
android {
namespace = "com.example.mymultimoduleapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.mymultimoduleapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4" // Убедитесь, что версия совместима с Kotlin
}
// ...
}
dependencies {
implementation(project(":core_data"))
implementation(project(":core_domain"))
implementation(project(":core_common_ui"))
implementation(project(":feature_home"))
implementation(project(":feature_profile"))
implementation(project(":feature_settings"))
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2024.05.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.7.7")
// Hilt
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-android-compiler:2.48.1")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Icons
implementation("androidx.compose.material:material-icons-core:1.6.7")
implementation("androidx.compose.material:material-icons-extended:1.6.7")
// ...
}
Пример для :core_data модуля:
// :core_data/build.gradle.kts
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}
android {
namespace = "com.example.core_data"
compileSdk = 34
// ...
}
dependencies {
implementation(project(":core_domain")) // Если модели домена используются здесь
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
// Hilt
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-android-compiler:2.48.1")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// ...
}
Аналогично настраиваются зависимости для других модулей (:core_domain, :core_common_ui, :feature_*), указывая только необходимые им библиотеки и зависимости от других модулей проекта.
Настройка Hilt
Hilt требует класс Application, аннотированный @HiltAndroidApp.
Класс Application (`:app` модуль)
// В :app/src/main/java/com/example/mymultimoduleapp/MyApplication.kt
package com.example.mymultimoduleapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application() {
// Можно добавить код инициализации, если требуется
}
Не забудьте указать этот класс в AndroidManifest.xml:
<!-- :app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mymultimoduleapp">
<application
android:name=".MyApplication"
... >
<!-- ... -->
</application>
</manifest>
Реализация Модулей
Модуль :core_data
Этот модуль отвечает за всю работу с данными.
Сетевой слой с Retrofit
Определяем интерфейс для API и Hilt-модуль для предоставления Retrofit.
// :core_data/src/main/java/.../network/ApiService.kt
package com.example.core_data.network
// Предположим, у нас есть такой data class для ответа сервера
data class PostDto(val id: Int, val title: String, val body: String)
interface ApiService {
@GET("posts")
suspend fun getPosts(): List<PostDto>
@GET("posts/{id}")
suspend fun getPostById(@Path("id") postId: Int): PostDto
}
// :core_data/src/main/java/.../di/NetworkModule.kt
package com.example.core_data.di
import com.example.core_data.network.ApiService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY // Используйте BASIC или NONE для release сборок
}
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/") // Ваш базовый URL
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Локальное хранилище с Room
Определяем Entity, DAO, Database класс и Hilt-модуль.
// :core_data/src/main/java/.../database/PostEntity.kt
package com.example.core_data.database
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "posts")
data class PostEntity(
@PrimaryKey val id: Int,
val title: String,
val body: String
)
// :core_data/src/main/java/.../database/PostDao.kt
package com.example.core_data.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface PostDao {
@Query("SELECT * FROM posts")
fun getAllPosts(): Flow<List<PostEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPosts(posts: List<PostEntity>)
@Query("DELETE FROM posts")
suspend fun deleteAllPosts()
}
// :core_data/src/main/java/.../database/AppDatabase.kt
package com.example.core_data.database
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [PostEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun postDao(): PostDao
}
// :core_data/src/main/java/.../di/DatabaseModule.kt
package com.example.core_data.di
import android.content.Context
import androidx.room.Room
import com.example.core_data.database.AppDatabase
import com.example.core_data.database.PostDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_app_database"
).build()
}
@Provides
@Singleton
fun providePostDao(appDatabase: AppDatabase): PostDao {
return appDatabase.postDao()
}
}
Репозиторий
Репозиторий абстрагирует источники данных.
// :core_data/src/main/java/.../repository/PostRepository.kt
package com.example.core_data.repository
import com.example.core_data.database.PostDao
import com.example.core_data.database.PostEntity // Предположим, PostEntity это модель для Room
import com.example.core_data.network.ApiService
import com.example.core_data.network.PostDto // Предположим, PostDto это модель для Retrofit
import com.example.core_domain.model.Post // Модель из domain слоя
import com.example.core_domain.repository.IPostRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
// Пример функции-маппера
fun PostDto.toEntity(): PostEntity = PostEntity(id, title, body)
fun PostEntity.toDomain(): Post = Post(id, title, body)
@Singleton
class PostRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val postDao: PostDao
) : IPostRepository {
override fun getPosts(): Flow<List<Post>> {
return postDao.getAllPosts().map { entities ->
entities.map { it.toDomain() }
}
}
override suspend fun refreshPosts() {
try {
val remotePosts = apiService.getPosts()
postDao.insertPosts(remotePosts.map { it.toEntity() })
} catch (e: Exception) {
// Обработка ошибок сети
e.printStackTrace()
}
}
// Другие методы, например, getPostById
}
Интерфейс IPostRepository должен быть определен в :core_domain.
Модуль :core_domain
Этот модуль содержит чистую бизнес-логику.
Доменные модели
// :core_domain/src/main/java/com/example/core_domain/model/Post.kt
package com.example.core_domain.model
data class Post(
val id: Int,
val title: String,
val body: String
)
Интерфейс Репозитория
// :core_domain/src/main/java/com/example/core_domain/repository/IPostRepository.kt
package com.example.core_domain.repository
import com.example.core_domain.model.Post
import kotlinx.coroutines.flow.Flow
interface IPostRepository {
fun getPosts(): Flow<List<Post>>
suspend fun refreshPosts()
// ... другие методы
}
Hilt-модуль для связывания реализации репозитория с интерфейсом (можно разместить в `:core_data` или в `:app`, если там происходит сборка):
// В :core_data/src/main/java/.../di/RepositoryModule.kt
package com.example.core_data.di
import com.example.core_data.repository.PostRepositoryImpl
import com.example.core_domain.repository.IPostRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindPostRepository(
postRepositoryImpl: PostRepositoryImpl
): IPostRepository
}
Сценарии использования (Use Cases)
// :core_domain/src/main/java/com/example/core_domain/usecase/GetPostsUseCase.kt
package com.example.core_domain.usecase
import com.example.core_domain.model.Post
import com.example.core_domain.repository.IPostRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetPostsUseCase @Inject constructor(
private val repository: IPostRepository
) {
operator fun invoke(): Flow<List<Post>> {
return repository.getPosts()
}
}
// :core_domain/src/main/java/com/example/core_domain/usecase/RefreshPostsUseCase.kt
package com.example.core_domain.usecase
import com.example.core_domain.repository.IPostRepository
import javax.inject.Inject
class RefreshPostsUseCase @Inject constructor(
private val repository: IPostRepository
) {
suspend operator fun invoke() {
repository.refreshPosts()
}
}
Модули Функциональности (:feature_*)
Каждый такой модуль содержит UI и ViewModel для конкретной фичи.
ViewModel (Пример для :feature_home)
// :feature_home/src/main/java/.../HomeScreenViewModel.kt
package com.example.feature_home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.core_domain.model.Post
import com.example.core_domain.usecase.GetPostsUseCase
import com.example.core_domain.usecase.RefreshPostsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
data class HomeUiState(
val posts: List<Post> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
private val getPostsUseCase: GetPostsUseCase,
private val refreshPostsUseCase: RefreshPostsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
loadPosts()
observePosts()
}
private fun observePosts() {
viewModelScope.launch {
getPostsUseCase().collect { posts ->
_uiState.value = _uiState.value.copy(posts = posts, isLoading = false)
}
}
}
fun loadPosts(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
try {
if (forceRefresh || _uiState.value.posts.isEmpty()) {
refreshPostsUseCase() // Загружает из сети и сохраняет в БД
}
// Данные будут обновлены через Flow из observePosts()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage ?: "Unknown error")
}
}
}
}
UI с Jetpack Compose (Пример для :feature_home)
// :feature_home/src/main/java/.../HomeScreen.kt
package com.example.feature_home
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun HomeScreen(
viewModel: HomeScreenViewModel = hiltViewModel(),
onNavigateToDetails: (postId: Int) -> Unit // Callback для навигации
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(title = { Text("Главная") })
}
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
if (uiState.isLoading && uiState.posts.isEmpty()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else if (uiState.error != null) {
Text(
text = "Ошибка: ${uiState.error}",
modifier = Modifier.align(Alignment.Center).padding(16.dp)
)
} else if (uiState.posts.isEmpty()) {
Text(
text = "Нет данных для отображения. Потяните для обновления.",
modifier = Modifier.align(Alignment.Center).padding(16.dp)
)
}
else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.posts) { post ->
PostItem(post = post, onClick = { onNavigateToDetails(post.id) })
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PostItem(post: com.example.core_domain.model.Post, onClick: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = post.title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text(text = post.body, style = MaterialTheme.typography.bodySmall, maxLines = 3)
}
}
}
Аналогично создаются экраны и ViewModel для :feature_profile и :feature_settings.
Модуль :app - Точка Входа и Навигация
MainActivity
// :app/src/main/java/.../MainActivity.kt
package com.example.mymultimoduleapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.*
import com.example.feature_home.HomeScreen // Импорт экранов из feature-модулей
import com.example.feature_profile.ProfileScreen
import com.example.feature_settings.SettingsScreen
// import com.example.your_app_theme.YourAppTheme // Замените на вашу тему
import dagger.hilt.android.AndroidEntryPoint
// Определение маршрутов навигации
sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object Home : Screen("home", "Главная", Icons.Filled.Home)
object Profile : Screen("profile", "Профиль", Icons.Filled.AccountCircle)
object Settings : Screen("settings", "Настройки", Icons.Filled.Settings)
object Details : Screen("details/{postId}", "Детали", Icons.Filled.Home) { // Пример экрана деталей
fun createRoute(postId: Int) = "details/$postId"
}
}
val bottomNavItems = listOf(
Screen.Home,
Screen.Profile,
Screen.Settings
)
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// YourAppTheme { // Оберните в вашу тему
AppNavigation()
// }
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavigation() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
bottomNavItems.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = screen.label) },
label = { Text(screen.label) },
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
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = Screen.Home.route,
modifier = Modifier.padding(innerPadding)
) {
composable(Screen.Home.route) {
HomeScreen(
onNavigateToDetails = { postId ->
navController.navigate(Screen.Details.createRoute(postId))
}
)
}
composable(Screen.Profile.route) { ProfileScreen(/* navController = navController */) }
composable(Screen.Settings.route) { SettingsScreen(/* navController = navController */) }
// Пример экрана деталей (может быть в своем feature-модуле)
composable(
route = Screen.Details.route,
arguments = listOf(navArgument("postId") { type = NavType.IntType })
) { backStackEntry ->
val postId = backStackEntry.arguments?.getInt("postId")
// DetailScreen(postId = postId, onBack = { navController.popBackStack() })
// Заглушка для экрана деталей:
Text("Экран Деталей для Post ID: $postId", modifier = Modifier.padding(16.dp))
}
}
}
}
На изображении ниже показан пример того, как может выглядеть нижний навигационный бар в приложении.
Пример Bottom Navigation Bar.
Сравнение Подходов: Многомодульность vs Монолит
Многомодульная архитектура предлагает значительные преимущества по сравнению с традиционным монолитным подходом, особенно по мере роста и усложнения проекта. Диаграмма ниже иллюстрирует сравнение по ключевым аспектам:
Как видно, многомодульный подход выигрывает по большинству параметров, способствуя созданию более качественных и легко управляемых приложений.
Взгляд изнутри: Разработка с Многомодульной Архитектурой
Это видео подробно рассматривает создание многомодульного проекта с использованием Jetpack Compose, что может дать вам дополнительное понимание процесса и преимуществ данного подхода.
В видео автор демонстрирует практические шаги по настройке модулей, зависимостей и навигации, что очень полезно для визуализации концепций, которые мы обсудили.
Ключевые Компоненты и Их Роли
В таблице ниже представлен обзор основных компонентов нашего многомодульного приложения и их распределение по модулям и зонам ответственности.
| Компонент | Модуль(-и) | Ответственность |
|---|---|---|
MainActivity |
:app |
Точка входа в приложение, хостинг NavHost и BottomNavigationBar. |
MyApplication |
:app |
Класс приложения, аннотированный @HiltAndroidApp для инициализации Hilt. |
NavHostController / NavHost |
:app |
Управление навигацией между экранами Compose. |
BottomNavigationBar (Composable) |
:app (или :core_common_ui) |
UI для переключения между основными экранами (Home, Profile, Settings). |
Screen (sealed class/object) |
:app (или общий навигационный модуль) |
Определение навигационных маршрутов и метаданных экранов. |
*ViewModel (e.g., HomeScreenViewModel) |
:feature_* (e.g., :feature_home) |
Подготовка и управление данными для UI, взаимодействие с UseCases, обработка бизнес-логики UI. |
*Screen (Composable-функции) |
:feature_* (e.g., :feature_home) |
Отображение пользовательского интерфейса, отправка пользовательских событий в ViewModel, подписка на изменения состояния. |
UseCase (e.g., GetPostsUseCase) |
:core_domain |
Инкапсуляция конкретной бизнес-логики, не зависящей от UI или источников данных. |
IPostRepository / PostRepositoryImpl |
:core_domain (интерфейс), :core_data (реализация) |
Абстракция над источниками данных (сеть, локальная БД), координация получения и сохранения данных. |
ApiService (Retrofit) |
:core_data |
Определение HTTP запросов к удаленному API. |
PostDao (Room DAO) |
:core_data |
Интерфейс доступа к данным (Data Access Object) для локальной базы данных Room. |
AppDatabase (Room Database) |
:core_data |
Основной класс базы данных Room, определяющий entities и DAOs. |
Hilt Modules (e.g., NetworkModule, DatabaseModule) |
:core_data, :app (и др.) |
Предоставление экземпляров зависимостей (Retrofit, Room, репозитории, UseCases и т.д.) с помощью Hilt. |
Часто Задаваемые Вопросы (FAQ)
Как добавить новый экран или фичу в эту многомодульную структуру?
1. Создайте новый feature-модуль (например, :feature_newscreen) через Android Studio (File > New > New Module > Android Library).
2. Настройте build.gradle.kts нового модуля, добавив необходимые зависимости (Compose, Hilt, ViewModel, зависимости от :core_domain, :core_common_ui).
3. Добавьте модуль в settings.gradle.kts корневого проекта и в зависимости модуля :app.
4. Реализуйте ViewModel и UI (Composable) в новом feature-модуле.
5. Определите новый маршрут в Screen sealed class (в модуле :app или общем навигационном модуле).
6. Добавьте composable-функцию для нового экрана в NavHost в MainActivity (модуль :app).
7. Если экран должен быть доступен из Bottom Navigation, добавьте его в список bottomNavItems.
В чем разница между implementation project(":module") и api project(":module") в Gradle?
implementation: Зависимость доступна только внутри модуля, который ее объявляет. Это предпочтительный способ, так как он сокращает время сборки, не перекомпилируя модули, которые зависят от текущего, если изменяется только внутренняя реализация зависимости.
api: Зависимость становится частью API текущего модуля и транзитивно доступна модулям, которые зависят от текущего. Используйте api, только если модуль экспортирует типы из этой зависимости в своем публичном API.
В большинстве случаев для межмодульных зависимостей в приложении следует использовать implementation для лучшей инкапсуляции и производительности сборки.
Как передавать данные между экранами в Navigation Compose?
Есть несколько способов:
1. Через маршрут (route): Простые данные (строки, числа) можно передавать как часть маршрута. Например: "details/{itemId}". При навигации: navController.navigate("details/123"). В Composable экрана: val itemId = backStackEntry.arguments?.getString("itemId").
2. Общая ViewModel: Для более сложных данных или если экраны тесно связаны, можно использовать ViewModel, привязанную к графу навигации или Activity. Экраны могут получать доступ к данным из этой общей ViewModel.
3. Передача аргументов через NavArguments: Navigation Compose позволяет определять аргументы с типами (navArgument("itemId") { type = NavType.IntType }).
4. Для передачи сложных объектов (Parcelable/Serializable): Хотя прямая передача сложных объектов через аргументы маршрута не всегда рекомендуется из-за ограничений на размер, можно сериализовать объект в JSON строку и передать ее, либо сохранить объект во временном хранилище (например, в репозитории или специальной ViewModel) и передать только его ID.
Нужен ли :core_domain модуль для очень простого приложения?
Для очень маленьких или прототипных приложений отдельный :core_domain модуль может показаться избыточным. В таких случаях бизнес-логику (UseCases) можно временно разместить в ViewModel или в :core_data модуле (например, если UseCase — это просто вызов метода репозитория).
Однако, выделение :core_domain имеет ряд преимуществ даже для средних проектов:
- Четкое разделение бизнес-логики: Делает код более понятным и тестируемым.
- Независимость от Android Framework: Доменные модели и UseCases — это чистый Kotlin, что упрощает их тестирование и переиспользование (например, в Kotlin Multiplatform).
- Подготовка к масштабированию: Если приложение будет расти, наличие доменного слоя облегчит добавление новой функциональности и поддержку существующей.
Решение зависит от конкретного проекта и планов на его развитие. Для учебных целей и демонстрации "чистой архитектуры" его включение полезно.
Рекомендуемые Запросы для Дальнейшего Изучения
- Как реализовать продвинутые сценарии навигации в Jetpack Compose, включая передачу сложных объектов между экранами?
- Какие стратегии и инструменты лучше всего подходят для тестирования UI, ViewModel и репозиториев в многомодульном Android приложении с Hilt и Compose?
- Какие существуют эффективные способы оптимизации времени сборки Gradle в крупных многомодульных Kotlin/Android проектах?
- Как эффективно управлять состоянием сложных экранов Jetpack Compose в многомодульной архитектуре, используя StateFlow, SharedViewModel или другие подходы?
Источники и Полезные Ссылки
Last updated May 12, 2025