Общие слои репозитория и вариантов использования
Переносите бизнес-логику и доступ к данным в commonMain для максимального повторного использования.
«Общие слои репозитория и вариантов использования» — бесплатный урок Kotlin Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Kotlin Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Kotlin Academy содержит 4 уроков всего.
Цель архитектуры
В KMP цель состоит в том, чтобы перенести как можно больше логики в commonMain: доменные модели, интерфейсы репозиториев, сценарии использования и ViewModels. Только платформенный ввод-вывод должен оставаться в наборах исходного кода платформ.
// Target architecture:
// commonMain:
// domain/ - data classes, interfaces
// data/ - repository implementations using shared clients
// usecase/ - business logic, composing repositories
// androidMain / iosMain:
// DI wiring, platform drivers (DB, network)Доменный слой в commonMain
Определяйте доменные сущности и интерфейсы репозиториев в commonMain. Импортов платформ быть не должно.
// commonMain/domain/User.kt
data class User(val id: String, val name: String, val email: String)
// commonMain/domain/UserRepository.kt
interface UserRepository {
suspend fun getUser(id: String): User?
suspend fun getAllUsers(): List<User>
suspend fun saveUser(user: User)
}Общая реализация репозитория с Ktor
Реализуйте репозиторий в commonMain с помощью клиента Ktor (мультиплатформенного). Фактический HTTP-движок предоставляется отдельно для каждой платформы.
// commonMain/data/RemoteUserRepository.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class RemoteUserRepository(private val client: HttpClient) : UserRepository {
override suspend fun getUser(id: String): User? =
client.get("https://api.example.com/users/$id").body()
override suspend fun getAllUsers(): List<User> =
client.get("https://api.example.com/users").body()
override suspend fun saveUser(user: User) {
client.post("https://api.example.com/users") { setBody(user) }
}
}Слой сценариев использования
Сценарии использования (интеракторы) координируют работу репозиториев и содержат бизнес-логику. Это чистый Kotlin без зависимостей от платформы.
// commonMain/usecase/GetUserUseCase.kt
class GetUserUseCase(private val repo: UserRepository) {
suspend operator fun invoke(id: String): Result<User> =
runCatching { repo.getUser(id) ?: error("User not found: $id") }
}
// commonMain/usecase/SaveUserUseCase.kt
class SaveUserUseCase(private val repo: UserRepository) {
suspend operator fun invoke(user: User): Result<Unit> =
runCatching { repo.saveUser(user) }
}Общая ViewModel
ViewModels могут находиться в commonMain и использовать kotlinx-coroutines и StateFlow. Android использует их напрямую, а iOS оборачивает их в SwiftUI.
// commonMain/viewmodel/UserViewModel.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class UserViewModel(private val getUser: GetUserUseCase) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val _user = MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
fun load(id: String) {
scope.launch { _user.value = getUser(id).getOrNull() }
}
fun clear() = scope.cancel()
}Конфигурация Ktor HttpClient
Создайте Ktor HttpClient в commonMain с общей конфигурацией. Движок (OkHttp или Darwin) внедряется либо создаётся отдельно для каждой платформы.
// commonMain/network/HttpClientFactory.kt
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
fun createHttpClient(engine: io.ktor.client.engine.HttpClientEngine? = null) =
HttpClient(engine ?: io.ktor.client.engine.cio.CIO) {
install(ContentNegotiation) { json() }
}SQLDelight для общего хранения данных
SQLDelight генерирует типобезопасные запросы Kotlin для всех платформ из файлов .sq в commonMain. Фактический драйвер базы данных зависит от платформы.
// commonMain/db/UserQueries.sq:
// CREATE TABLE User (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL);
// selectAll:
// SELECT * FROM User;
// Usage in commonMain:
// val db = UserDatabase(driver)
// val users = db.userQueries.selectAll().executeAsList()Связывание DI на платформе
Объединяйте все компоненты в платформенном модуле DI. Интерфейс является общим, различаются только аргументы конструктора.
// androidMain:
val androidModule = module {
single { AndroidSqliteDriver(UserDatabase.Schema, androidContext(), "users.db") }
single { UserDatabase(get()) }
single<UserRepository> { RemoteUserRepository(get()) }
single { GetUserUseCase(get()) }
viewModel { UserViewModel(get()) }
}
// iosMain:
// Same pattern with NativeSqliteDriverТестирование сценариев использования в commonTest
Сценарии использования не зависят от платформы, поэтому их легко тестировать модульными тестами в commonTest с помощью фиктивного репозитория.
// commonTest/usecase/GetUserUseCaseTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
class FakeUserRepo : UserRepository {
override suspend fun getUser(id: String) = if (id == "1") User("1", "Alice", "a@b.com") else null
override suspend fun getAllUsers() = emptyList<User>()
override suspend fun saveUser(user: User) {}
}
class GetUserUseCaseTest {
private val useCase = GetUserUseCase(FakeUserRepo())
@Test
fun testGetExistingUser() = kotlinx.coroutines.runBlocking {
val result = useCase("1")
assertEquals("Alice", result.getOrNull()?.name)
}
}Обработка ошибок в сценариях использования
Возвращайте из сценариев использования результат типа T или запечатанный класс, чтобы передавать ошибки без исключений через границу между общей и платформенной частями.
// commonMain:
sealed class UserResult {
data class Success(val user: User) : UserResult()
data class NotFound(val id: String) : UserResult()
data class Error(val cause: Throwable) : UserResult()
}
class SafeGetUser(private val repo: UserRepository) {
suspend operator fun invoke(id: String): UserResult = try {
val user = repo.getUser(id)
if (user != null) UserResult.Success(user) else UserResult.NotFound(id)
} catch (e: Exception) { UserResult.Error(e) }
}Общий код без отказа от платформенных идиом
Делитесь логикой, а не пользовательским интерфейсом. Android использует Compose, а iOS — SwiftUI. Обе платформы наблюдают за состоянием общей ViewModel через платформенные привязки.
// Android (Compose):
// val user by viewModel.user.collectAsState()
// iOS (SwiftUI):
// @State var user = viewModel.user.value
// viewModel.load(id: "1")
// Shared ViewModel:
// _user.value = ... (commonMain)
fun main() { println("Share logic; use platform UI frameworks") }Быстрая проверка
В каком слое хорошо структурированного проекта KMP не должно быть платформенных импортов?
Итоги
Размещайте доменные модели, интерфейсы репозиториев, сценарии использования и общие ViewModels в commonMain. Используйте Ktor (сеть) и SQLDelight (база данных), внедряя платформенные движки на уровне DI. Тестируйте бизнес-логику в commonTest с помощью фиктивных репозиториев.
Часто задаваемые вопросы
Урок «Общие слои репозитория и вариантов использования» бесплатный?
Да — полный текст урока «Общие слои репозитория и вариантов использования» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Kotlin Academy, подпишись на CoddyKit PRO. Курс Kotlin Academy содержит 4 уроков всего.
Чему я научусь в уроке «Общие слои репозитория и вариантов использования»?
Переносите бизнес-логику и доступ к данным в commonMain для максимального повторного использования. Ты практикуешь Kotlin Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Kotlin Academy?
Предыдущий опыт не требуется. Kotlin Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Общие слои репозитория и вариантов использования»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Kotlin Academy?
Да. Каждый урок Kotlin Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Структура проекта KMP: commonMain, androidMain, iosMain
- Механизм expect/actual для API платформ
- Общие слои репозитория и вариантов использования
- Внедрение зависимостей в KMP с Koin