0Pricing
Kotlin Academy · Урок

Общие слои репозитория и вариантов использования

Переносите бизнес-логику и доступ к данным в 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 — локальная установка не требуется.

Все уроки этого курса

  1. Структура проекта KMP: commonMain, androidMain, iosMain
  2. Механизм expect/actual для API платформ
  3. Общие слои репозитория и вариантов использования
  4. Внедрение зависимостей в KMP с Koin
← Назад к Kotlin Academy