Какие знаешь способы проектирования архитектуры KMP-приложения?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Архитектурные подходы для KMP-приложения (Kotlin Multiplatform)
Проектирование архитектуры Kotlin Multiplatform (KMP) приложения требует баланса между общими бизнес-логикой для всех целевых платформ (Android, iOS, Desktop, Web) и специфичными реализациями для каждой из них. Вот основные подходы и практики, которые я использую:
1. Стратегия разделения модулей: общие (shared) и платформные (platform)
Ключевой принцип — четкое разделение кода на common (общий) и platform-specific (платформно-специфичный) модули.
// Пример структуры проекта в Gradle:
// build.gradle.kts (общий модуль)
kotlin {
android()
ios()
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib"))
// Общие KMP-библиотеки, например, korlibs, ktor-client
}
}
val androidMain by getting {
dependencies {
// Android-специфичные зависимости
}
}
val iosMain by getting {
dependencies {
// iOS-специфичные зависимости
}
}
}
}
- Общий модуль (
commonMain) содержит:
- Бизнес-логику (Use Cases, Interactors).
- Модели данных (Data Classes, Entities).
- Репозитории (интерфейсы или абстрактные классы).
- Источники данных (интерфейсы для работы с сетью или базой данных).
- Утилиты и расширения (pure Kotlin код).
- Платформные модули (
androidMain,iosMainи др.) реализуют:
- Конкретные реализации репозиториев (например, с использованием `OkHttp` на Android и `NSURLSession` на iOS).
- UI-компоненты и навигацию (полностью платформно-специфичные).
- Интеграцию с платформными API (уведомления, файловая система, Bluetooth).
2. Архитектурные паттерны в общем модуле
Для организации кода в common модуле я применяю проверенные паттерны, адаптированные для KMP.
MVVM (Model-View-ViewModel) или MVI (Model-View-Intent)
// commonMain: Пример ViewModel (или Presenter/Controller)
class ProductsViewModel(
private val productsRepository: ProductsRepository
) {
private val _state = MutableStateFlow(ProductsState.Loading)
val state: StateFlow<ProductsState> = _state.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_state.value = ProductsState.Loading
try {
val products = productsRepository.getAll()
_state.value = ProductsState.Success(products)
} catch (e: Exception) {
_state.value = ProductsState.Error(e.message)
}
}
}
}
// Общие State и Model
sealed class ProductsState {
data class Success(val products: List<Product>) : ProductsState()
data class Error(val message: String?) : ProductsState()
object Loading : ProductsState()
}
- ViewModel остается в общем модуле, но использует корутины (
CoroutineScope) черезkotlinx.coroutines. Для iOS потребуется дополнительная адаптация (MainDispatcher). - State определяется как sealed классы или интерфейсы в common модуле.
Clean Architecture + многослойность
Я разделяю общий модуль на слои:
- Domain Layer: Entities, Use Cases (Interactors), интерфейсы Repository.
- Data Layer: Реализации Repository, Sources (Network, Database), DTOs.
- Presentation Layer: ViewModels, States, Events.
// Пример UseCase в Domain Layer
class GetProductsUseCase(
private val repository: ProductsRepository
) {
suspend operator fun invoke(): Result<List<Product>> {
return try {
Result.success(repository.getAll())
} catch (e: Exception) {
Result.failure(e)
}
}
}
3. Dependency Injection (DI) в KMP
Управление зависимостями критично для тестирования и замены платформных реализаций.
- Manual DI (ручная инъекция): простой подход для небольших проектов, где зависимости создаются вручную в платформных модулях.
- KMP-библиотеки для DI: такие как Koin (есть KMP версия) или Dagger (через общие интерфейсы, но реализации на платформах). Однако, нужно учитывать, что не все библиотеки полноценно поддерживают KMP.
// Пример с Koin для common модуля
val commonModule = module {
single<ProductsRepository> { ProductsRepositoryImpl(get()) }
factory { ProductsViewModel(get()) }
}
// На Android: androidApp.module инициализирует Koin с commonModule
// На iOS: нужно использовать Koin в Swift через Kotlin/Native export
4. Работа с платформно-специфичным кодом: ожидания (expect) и реализации (actual)
Kotlin Multiplatform предоставляет механизм expect/actual для объявления общих интерфейсов (ожиданий) и их платформных реализаций.
// commonMain: объявляем expect класс или функцию
expect class PlatformFileSystem {
fun readFile(path: String): String
}
// androidMain: реализация для Android
actual class PlatformFileSystem {
actual fun readFile(path: String): String {
// Используем Android API (java.io.File)
return File(path).readText()
}
}
// iosMain: реализация для iOS
actual class PlatformFileSystem {
actual fun readFile(path: String): String {
// Используем iOS API (Swift/Objective-C через Kotlin/Native interop)
// Здесь может быть C-код или вызов в Swift
return ""
}
}
Этот подход идеально подходит для:
- Управления файловой системой.
- Работы с сетью (создание HTTP-клиента).
- Локальных хранилищ (Keychain на iOS, SharedPreferences на Android).
- Получения информации о устройстве (версия OS, разрешение дисплея).
5. Тестирование и инструменты
Архитектура должна позволять тестирование:
- Unit-тесты в common модуле: тестируем бизнес-логику, Use Cases, репозитории (с моками).
- Интеграционные тесты на платформах: проверяем, что платформные реализации корректно работают с common кодом.
Инструменты:
- Gradle Multiplatform Configuration: настройка sourceSets, зависимостей.
- Kotlin/Native и Swift interoperability: важно для iOS стороны (использование
cinterop, генерация Objective-C заголовков). - CI/CD: сборка и тестирование всех целевых платформ автоматически.
Выводы и рекомендации
Выбор архитектуры зависит от масштаба проекта:
- Для простых приложений с фокусом на бизнес-логике: MVVM + expect/actual для ключевых платформных операций.
- Для комплексных продуктов: Clean Architecture с четким разделением слоев, DI библиотека (Koin) и возможно MVI для более строгого управления состоянием.
Главное — максимально изолировать общую логику от платформных деталей, используя интерфейсы и абстракции, чтобы обеспечить легкую замену и тестирование. Также важно продумать навигацию и UI, которые остаются полностью платформными, но могут быть связаны с общими ViewModel через общие State и Events.