Создание мультиплатформенного приложения с помощью Ktor и SQLDelight
В этом руководстве показано, как с помощью IntelliJ IDEA создать продвинутое мобильное приложение для iOS и Android на Kotlin Multiplatform. Это приложение будет выполнять следующие задачи:
Получать данные через интернет из общедоступной Launch Library с помощью Ktor
Сохранять данные в локальной базе данных с помощью SQLDelight.
Отображать список запусков космических ракет вместе с датой запуска, результатами и подробным описанием запуска.
В приложение войдёт модуль с общим кодом для платформ iOS и Android. Бизнес-логика и уровни доступа к данным будут реализованы только один раз в общем модуле, а пользовательский интерфейс обоих приложений будет нативным.

В проекте будут использоваться следующие мультиплатформенные библиотеки:
Ktor в качестве HTTP-клиента для получения данных через интернет.
kotlinx.serializationдля десериализации ответов JSON в объекты классов сущностей.kotlinx.coroutinesдля написания асинхронного кода.SQLDelight для генерации кода Kotlin из SQL-запросов и создания типобезопасного API базы данных.
Koin для предоставления драйверов базы данных для конкретных платформ с помощью внедрения зависимостей.
Создание проекта
В кратком руководстве выполните инструкции по настройке среды для разработки на Kotlin Multiplatform.
В IntelliJ IDEA выберите Файл | Создать | Проект.
На панели слева выберите Kotlin Multiplatform (в Android Studio шаблон находится на вкладке Общие мастера Создание проекта).
-
В окне Новый проект укажите следующие параметры:
Название: SpaceTutorial
Идентификатор проекта: com.jetbrains.spacetutorial
Выберите целевые платформы Android и iOS.
Для iOS выберите параметр Не предоставлять общий доступ к пользовательскому интерфейсу. Для обеих платформ вы реализуете нативный пользовательский интерфейс.
-
Указав все параметры и целевые платформы, нажмите Создать.

Добавление зависимостей Gradle
Чтобы добавить мультиплатформенную библиотеку в общий модуль, добавьте инструкции для зависимостей (implementation) в блок dependencies {} соответствующих наборов исходного кода в файле build.gradle.kts модуля.
Для библиотек kotlinx.serialization и SQLDelight также требуется дополнительная настройка.
Измените или добавьте строки в каталоге версий в файле gradle/libs.versions.toml, указав все необходимые зависимости:
-
В блоке
[versions]проверьте версию AGP и добавьте остальные параметры:[versions] agp = "9.0.1" material3 = "1.11.0-alpha07" # ... coroutinesVersion = "1.11.0" dateTimeVersion = "0.8.0" koin = "4.2.2" ktor = "3.5.2" sqlDelight = "2.3.2" -
В блоке
[libraries]добавьте следующие ссылки на библиотеки:[libraries] ... koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "dateTimeVersion" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } -
В блоке
[plugins]укажите необходимые плагины Gradle:[plugins] # ... kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" } После обновления каталога версий появится запрос на повторную синхронизацию проекта. Нажмите кнопку Синхронизировать изменения Gradle, чтобы синхронизировать файлы Gradle:
-
В самом начале файла
sharedLogic/build.gradle.ktsдобавьте следующие строки в блокplugins {}:plugins { // ... alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.sqldelight) } -
Для общего набора исходного кода требуется основной артефакт каждой библиотеки, а также функция сериализации Ktor для использования
kotlinx.serialization. Для наборов исходного кода iOS и Android также нужны драйверы SQLDelight и Ktor для соответствующих платформ.В том же файле
sharedLogic/build.gradle.ktsдобавьте все необходимые зависимости:kotlin { // ... sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.sqldelight.runtime) implementation(libs.kotlinx.datetime) implementation(libs.koin.core) } androidMain.dependencies { implementation(libs.ktor.client.android) implementation(libs.sqldelight.android.driver) } iosMain.dependencies { implementation(libs.ktor.client.darwin) implementation(libs.sqldelight.native.driver) } } } Указав зависимости, ещё раз нажмите кнопку Синхронизировать изменения Gradle, чтобы обновить файлы Gradle.
После синхронизации Gradle настройка проекта завершена, и можно приступать к написанию кода.
Создание модели данных приложения
Приложение из руководства будет содержать общедоступный класс SpaceSDK, который служит фасадом для сетевых сервисов и сервисов кэширования. Модель данных приложения будет включать три класса сущностей со следующими данными:
Общие сведения о запуске
Ссылки на изображения эмблем миссий
URL-адреса статей, связанных с запуском
Создайте необходимые классы данных:
В каталоге
sharedLogic/src/commonMain/kotlin/com/jetbrains/spacetutorialсоздайте пакетentity, а затем файлEntity.ktв этом пакете.-
Объявите все классы данных для основных сущностей:
import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.time.Instant @Serializable data class LaunchStatus( @SerialName("id") val id: Int, @SerialName("name") val name: String, @SerialName(value = "description") val description: String ) @Serializable data class LaunchListResponse( @SerialName("results") val results: List<RocketLaunch>, ) @Serializable data class RocketLaunch( @SerialName("id") val id: String, @SerialName("name") val missionName: String, @SerialName("net") val launchDateUTC: String, @SerialName(value = "image") val image: Image, @SerialName(value = "status") val status: LaunchStatus, ) { var launchYear = Instant.parse(launchDateUTC).toLocalDateTime(TimeZone.UTC).year } @Serializable data class Image( @SerialName("thumbnail_url") val small: String, @SerialName("image_url") val large: String, )
Каждый сериализуемый класс должен быть помечен аннотацией @Serializable. Плагин kotlinx.serialization автоматически генерирует сериализатор по умолчанию для классов @Serializable, если только вы явно не укажете ссылку на сериализатор в аргументе аннотации.
Аннотация @SerialName позволяет переопределять имена полей, что помогает обращаться к свойствам классов данных с помощью более понятных идентификаторов.
Настройка SQLDelight и реализация логики кэширования
Библиотека SQLDelight позволяет генерировать типобезопасный API базы данных Kotlin из SQL-запросов. Во время компиляции генератор проверяет SQL-запросы и преобразует их в код Kotlin, который можно использовать в общем модуле.
Настройка SQLDelight
Зависимость SQLDelight уже добавлена в проект. Чтобы настроить библиотеку, откройте файл sharedLogic/build.gradle.kts и добавьте в его конец блок sqldelight {}. Этот блок содержит список баз данных и их параметры:
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.jetbrains.spacetutorial.cache")
}
}
}
Параметр packageName задаёт имя пакета для сгенерированных исходных файлов Kotlin.
Когда появится соответствующий запрос, синхронизируйте файлы проекта Gradle или дважды нажмите Shift и найдите действие Синхронизировать все проекты Gradle и Swift Package Manager.
Генерация API базы данных
Сначала создайте файл .sq со всеми необходимыми SQL-запросами. По умолчанию плагин SQLDelight ищет файлы .sq в папке sqldelight набора исходного кода:
В каталоге
sharedLogic/src/commonMainсоздайте новый каталогsqldelight.В каталоге
sqldelightсоздайте каталог с именемcom/jetbrains/spacetutorial/cache, чтобы сформировать вложенные каталоги для пакета.В каталоге
cacheсоздайте файлAppDatabase.sq(с тем же именем, что и у базы данных, указанным в файлеbuild.gradle.kts). В этом файле будут храниться все SQL-запросы приложения.-
В базе данных будет таблица со сведениями о запусках. Добавьте следующий код в файл
AppDatabase.sq, чтобы создать таблицу и определить несколько функций, которые вы будете использовать позже:import kotlin.Boolean; CREATE TABLE Launch ( flightNumber TEXT NOT NULL, missionName TEXT NOT NULL, launchDateUTC TEXT NOT NULL, imageSmall TEXT NOT NULL, imageLarge TEXT NOT NULL, statusId INTEGER NOT NULL, statusName TEXT NOT NULL, statusDescription TEXT NOT NULL ); insertLaunch: INSERT INTO Launch(flightNumber, missionName, launchDateUTC, imageSmall, imageLarge, statusId, statusName, statusDescription) VALUES(?, ?, ?, ?, ?, ?, ?, ?); removeAllLaunches: DELETE FROM Launch; selectAllLaunchesInfo: SELECT Launch.* FROM Launch; -
Сгенерируйте соответствующий интерфейс
AppDatabase(позже вы инициализируете его с помощью драйверов базы данных). Для этого выполните следующую команду в терминале из корневого каталога проекта:./gradlew generateCommonMainAppDatabaseInterface
Сгенерированный код Kotlin сохраняется в каталоге
sharedLogic/build/generated/sqldelight.
Создание фабрик драйверов базы данных для конкретных платформ
Чтобы инициализировать интерфейс AppDatabase, передайте ему экземпляр SqlDriver. SQLDelight предоставляет несколько реализаций драйвера SQLite для разных платформ, поэтому для каждой платформы необходимо создать отдельный экземпляр.
Хотя этого можно добиться с помощью ожидаемых и фактических интерфейсов, в этом проекте вы воспользуетесь Koin, чтобы попробовать внедрение зависимостей в Kotlin Multiplatform.
Создайте интерфейс для драйверов базы данных. Для этого создайте пакет
cacheв каталогеsharedLogic/src/commonMain/kotlin/com/jetbrains/spacetutorial/.-
Создайте интерфейс
DatabaseDriverFactoryв пакетеcache:package com.jetbrains.spacetutorial.cache import app.cash.sqldelight.db.SqlDriver interface DatabaseDriverFactory { fun createDriver(): SqlDriver } Создайте класс, реализующий этот интерфейс для Android: в каталоге
sharedLogic/src/androidMain/kotlinсоздайте пакетcom.jetbrains.spacetutorial.cache, а затем файлAndroidDatabaseDriverFactory.ktв этом пакете.-
В Android драйвер SQLite реализован классом
AndroidSqliteDriver. В файлеDatabaseDriverFactory.ktпередайте сведения о базе данных и ссылку на контекст в конструктор классаAndroidSqliteDriver:package com.jetbrains.spacetutorial.cache import android.content.Context import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver class AndroidDatabaseDriverFactory(private val context: Context) : DatabaseDriverFactory { override fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "launch.db") } } Для iOS создайте пакет
cacheв каталогеshared/src/iosMain/kotlin/com/jetbrains/spacetutorial/.-
В пакете
cacheсоздайте файлDatabaseDriverFactory.ktи добавьте следующий код:package com.jetbrains.spacetutorial.cache import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver class IOSDatabaseDriverFactory : DatabaseDriverFactory { override fun createDriver(): SqlDriver { return NativeSqliteDriver(AppDatabase.Schema, "launch.db") } }
Позже вы будете использовать эти фабрики в частях проекта, специфичных для платформ.
Реализация кэша
Вы уже добавили фабрики драйверов базы данных для разных платформ и интерфейс AppDatabase для выполнения операций с базой данных. Теперь создайте класс Database, который будет обёрткой для интерфейса AppDatabase и будет содержать логику кэширования.
В общем наборе исходного кода
sharedLogic/src/commonMain/kotlinсоздайте классDatabaseв пакетеcom.jetbrains.spacetutorial.cache. Он будет содержать общую для обеих платформ логику.-
Чтобы предоставить драйвер для
AppDatabase, передайте абстрактный экземплярDatabaseDriverFactoryв конструктор классаDatabase:package com.jetbrains.spacetutorial.cache internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver()) private val dbQuery = database.appDatabaseQueries }Для этого класса задан уровень видимости internal, то есть он доступен только внутри мультиплатформенного модуля.
-
Внутри класса
Databaseреализуйте обработку данных. Сначала создайте функциюgetAllLaunches(), которая возвращает список всех запусков ракет. ФункцияmapLaunchSelecting()используется для преобразования результата запроса к базе данных в объектыRocketLaunch:import com.jetbrains.spacetutorial.entity.Image import com.jetbrains.spacetutorial.entity.LaunchStatus import com.jetbrains.spacetutorial.entity.RocketLaunch internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver()) private val dbQuery = database.appDatabaseQueries internal fun getAllLaunches(): List<RocketLaunch> { return dbQuery.selectAllLaunchesInfo(::mapLaunchSelecting).executeAsList() } private fun mapLaunchSelecting( flightNumber: String, missionName: String, launchDateUTC: String, imageSmall: String, imageLarge: String, statusId: Long, statusName: String, statusDescription: String ): RocketLaunch { return RocketLaunch( id = flightNumber, missionName = missionName, launchDateUTC = launchDateUTC, image = Image( small = imageSmall, large = imageLarge ), status = LaunchStatus( id = statusId.toInt(), name = statusName, description = statusDescription ) ) } } -
Добавьте функцию
clearAndCreateLaunches(), чтобы очистить базу данных и вставить новые данные:internal class Database(databaseDriverFactory: DatabaseDriverFactory) { // ... internal fun clearAndCreateLaunches(launches: List<RocketLaunch>) { dbQuery.transaction { dbQuery.removeAllLaunches() launches.forEach { launch -> dbQuery.insertLaunch( flightNumber = launch.id, missionName = launch.missionName, launchDateUTC = launch.launchDateUTC, imageSmall = launch.image.small, imageLarge = launch.image.large, statusId = launch.status.id.toLong(), statusName = launch.status.name, statusDescription = launch.status.description, ) } } } }
Реализация службы API
Для получения данных через интернет вы будете использовать общедоступный API Launch Library и один метод для получения списка всех запусков из конечной точки /2.3.0/launches.
Создайте класс для подключения приложения к API:
В каталоге
sharedLogic/src/commonMain/kotlin/com/jetbrains/spacetutorial/создайте пакетnetwork.-
В каталоге
networkсоздайте классSpaceApi:package com.jetbrains.spacetutorial.network import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json class SpaceApi { private val httpClient = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true useAlternativeNames = false }) } } }Этот класс выполняет сетевые запросы и десериализует ответы JSON в сущности из пакета
com.jetbrains.spacetutorial.entity. Экземпляр KtorHttpClientинициализирует и сохраняет свойствоhttpClient.В этом коде используется плагин Ktor
ContentNegotiationдля десериализации результата запросаGET. Плагин обрабатывает запрос и полезную нагрузку ответа как JSON, выполняя сериализацию и десериализацию по мере необходимости. -
Объявите функцию получения данных, возвращающую список запусков ракет:
import com.jetbrains.spacetutorial.entity.RocketLaunch import com.jetbrains.spacetutorial.entity.LaunchListResponse import io.ktor.client.request.get import io.ktor.client.call.body class SpaceApi { // ... suspend fun getAllLaunches(): List<RocketLaunch> { return (httpClient.get("https://lldev.thespacedevs.com/2.3.0/launches/previous/?mode=list&format=json").body() as LaunchListResponse).results } }
Функция getAllLaunches имеет модификатор suspend, поскольку содержит вызов suspend-функции HttpClient.get(). Функция HttpClient.get() выполняет асинхронную операцию для получения данных через интернет и может вызываться только из сопрограммы или другой suspend-функции. Сетевой запрос будет выполняться в пуле потоков HTTP-клиента.
URL-адрес для отправки GET-запроса передаётся в качестве аргумента функции get().
Создание SDK
Ваши приложения для iOS и Android будут взаимодействовать с космическим API через общий модуль, который предоставит общедоступный класс SpaceSDK.
-
В общем наборе исходного кода
sharedLogic/src/commonMain/kotlin, в пакетеcom.jetbrains.spacetutorialсоздайте классSpaceSDK. Этот класс будет фасадом для классовDatabaseиSpaceApi.Чтобы создать экземпляр класса
Database, предоставьте экземплярDatabaseDriverFactory:package com.jetbrains.spacetutorial import com.jetbrains.spacetutorial.cache.Database import com.jetbrains.spacetutorial.cache.DatabaseDriverFactory import com.jetbrains.spacetutorial.network.SpaceApi class SpaceSDK(databaseDriverFactory: DatabaseDriverFactory, val api: SpaceApi) { private val database = Database(databaseDriverFactory) }Внедрите подходящий драйвер базы данных в коде для конкретной платформы через конструктор класса
SpaceSDK. -
Добавьте функцию
getLaunches, которая использует созданную базу данных и API для получения и сохранения списка запусков:import com.jetbrains.spacetutorial.entity.RocketLaunch class SpaceSDK(databaseDriverFactory: DatabaseDriverFactory, val api: SpaceApi) { // ... @Throws(Exception::class) suspend fun getLaunches(forceReload: Boolean): List<RocketLaunch> { val cachedLaunches = database.getAllLaunches() return if (cachedLaunches.isNotEmpty() && !forceReload) { cachedLaunches } else { api.getAllLaunches().also { database.clearAndCreateLaunches(it) } } } }
Класс содержит одну функцию для получения всех сведений о запусках. В зависимости от значения forceReload она возвращает кэшированные значения или загружает данные из интернета, а затем обновляет кэш полученными результатами. Если кэшированных данных нет, данные загружаются из интернета независимо от значения флага forceReload.
Клиенты SDK могут использовать флаг forceReload для загрузки актуальных сведений о запусках, добавив для пользователей жест обновления потягиванием.
Все исключения Kotlin являются непроверяемыми, тогда как в Swift есть только проверяемые ошибки (подробности см. в разделе Взаимодействие со Swift/Objective-C). Поэтому, чтобы код Swift мог обрабатывать ожидаемые исключения, функции Kotlin, вызываемые из Swift, должны быть помечены аннотацией @Throws со списком возможных классов исключений.
Создание приложения Android
IntelliJ IDEA выполнит первоначальную настройку Gradle за вас, поэтому модули sharedUI и sharedLogic уже подключены к вашему приложению Android (androidApp).
Синхронизируйте файлы проекта Gradle, когда появится соответствующий запрос, или нажмите дважды Shift и найдите действие Синхронизировать все проекты Gradle и Swift Package Manager.
Добавление разрешения на доступ к интернету для androidApp
Чтобы получить доступ к интернету, приложению Android требуется соответствующее разрешение. Добавьте тег <uses-permission> в файл androidApp/src/main/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!--...-->
</manifest>
Добавление кода внедрения зависимостей
Внедрение зависимостей с помощью Koin позволяет объявлять модули (наборы компонентов), которые можно использовать в разных контекстах. В этом проекте вы создадите два модуля: один для приложения Android, а другой для приложения iOS. Затем вы запустите Koin для каждого нативного пользовательского интерфейса, используя соответствующий модуль.
Объявите модуль Koin, который будет содержать компоненты приложения Android:
-
Добавьте зависимость Koin Android для исходного набора
androidMainв файлsharedUI/build.gradle.kts:kotlin { // ... sourceSets { androidMain.dependencies { // ... implementation(libs.koin.androidx.compose) } } } Создайте каталог
sharedUI/src/androidMain/kotlinдля кода пользовательского интерфейса, предназначенного только для Android.В каталоге
sharedUI/src/androidMain/kotlinсоздайте пакетcom.jetbrains.spacetutorial.Удалите исходные наборы
commonMainиcommonTestиз модулейsharedUI, поскольку пользовательский интерфейс Android не будет общим.-
В пакете
sharedUI/src/androidMain/kotlin/com.jetbrains.spacetutorialсоздайте файлAppModule.kt.В этом файле объявите модуль Koin с двумя синглтонами: один для класса
SpaceApi, а другой — для классаSpaceSDK:import com.jetbrains.spacetutorial.cache.AndroidDatabaseDriverFactory import com.jetbrains.spacetutorial.network.SpaceApi import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val appModule = module { single<SpaceApi> { SpaceApi() } single<SpaceSDK> { SpaceSDK( databaseDriverFactory = AndroidDatabaseDriverFactory(androidContext()), api = get() ) } }Конструктор класса
SpaceSDKполучает внедряемый классAndroidDatabaseDriverFactory, специфичный для платформы. Функцияget()разрешает зависимости в модуле: вместо параметраapiдляSpaceSDK()Koin передаст ранее объявленный синглтонSpaceApi. -
В файле
androidApp/build.gradle.ktsдобавьте зависимость Koin Android для модуляandroidApp:kotlin { // ... dependencies { // ... implementation(libs.koin.androidx.compose) } } -
В модуле
androidApp, в каталогеsrc/main/kotlin/com/jetbrains/spacetutorial, создайте классMainApplication, который будет запускать модуль Koin.Передайте модуль, объявленный в файле
AppModule.kt, функцииmodules():package com.jetbrains.spacetutorial import android.app.Application import org.koin.android.ext.koin.androidContext import org.koin.core.context.GlobalContext.startKoin class MainApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MainApplication) modules(appModule) } } } -
Укажите созданный класс
MainApplicationв теге<application>файлаAndroidManifest.xml:<manifest xmlns:android="http://schemas.android.com/apk/res/android"> ... <application ... android:name="com.jetbrains.spacetutorial.MainApplication"> ... </application> </manifest>
Теперь вы готовы реализовать пользовательский интерфейс, который будет использовать данные, предоставляемые платформозависимым драйвером базы данных.
Подготовка модели представления со списком запусков
Вы реализуете пользовательский интерфейс Android с помощью Jetpack Compose и Material 3. Сначала вы создадите модель представления, которая использует SDK для получения списка запусков. Затем настроите тему Material и, наконец, напишете компонуемую функцию, которая объединит все это.
-
В каталоге
sharedUI/src/androidMain/kotlin, в пакетеcom.jetbrains.spacetutorial, создайте файлRocketLaunchViewModel.kt:package com.jetbrains.spacetutorial import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.jetbrains.spacetutorial.entity.RocketLaunch class RocketLaunchViewModel(private val sdk: SpaceSDK) : ViewModel() { private val _state = mutableStateOf(RocketLaunchScreenState()) val state: State<RocketLaunchScreenState> = _state } data class RocketLaunchScreenState( val isLoading: Boolean = false, val launches: List<RocketLaunch> = emptyList() )Экземпляр
RocketLaunchScreenStateбудет хранить данные, полученные из SDK, и текущее состояние запроса. -
Добавьте в класс
RocketLaunchViewModelфункциюloadLaunches, которая вызовет функциюgetLaunchesиз SDK в области корутин этой модели представления:import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class RocketLaunchViewModel(private val sdk: SpaceSDK) : ViewModel() { //... fun loadLaunches() { viewModelScope.launch { _state.value = _state.value.copy(isLoading = true, launches = emptyList()) try { val launches = sdk.getLaunches(forceReload = true) _state.value = _state.value.copy(isLoading = false, launches = launches) } catch (_: Exception) { _state.value = _state.value.copy(isLoading = false, launches = emptyList()) } } } } -
В классе
RocketLaunchViewModelдобавьте блокinit {}с вызовомloadLaunches(), чтобы запросить данные из API сразу после создания объектаRocketLaunchViewModel:class RocketLaunchViewModel(private val sdk: SpaceSDK) : ViewModel() { // ... init { loadLaunches() } } -
Теперь укажите модель представления в модуле Koin в файле
AppModule.kt:import org.koin.core.module.dsl.viewModel val appModule = module { // ... viewModel { RocketLaunchViewModel(sdk = get()) } }
Создание темы Material
Вы создадите основной компонуемый элемент App() вокруг функции AppTheme, предоставляемой темой Material:
Вы можете создать тему для приложения Compose с помощью конструктора тем Material. Выберите цвета и шрифты, затем нажмите Экспортировать тему в правом нижнем углу.
На экране экспорта нажмите раскрывающийся список Экспорт и выберите пункт Jetpack Compose (Theme.kt).
-
Распакуйте архив и скопируйте папку
themeв каталогsharedUI/src/androidMain/kotlin/com/jetbrains/spacetutorial: -
В каждом файле пакета
themeзамените строкуpackage, чтобы она указывала на созданный вами пакет:package com.jetbrains.spacetutorial.theme
-
В файле
Color.ktдобавьте две переменные для цветов, которые будут использоваться для успешных и неуспешных запусков:val app_theme_successful = Color(0xff4BB543) val app_theme_unsuccessful = Color(0xffFC100D)
Реализация логики представления
Создайте основной компонуемый элемент App() для приложения и вызовите его из класса ComponentActivity:
Создайте файл
App.ktв каталогеsharedUI/src/androidApp/kotlin/com/jetbrains/spacetutorial.-
Откройте файл
App.ktи вставьте следующий код:package com.jetbrains.spacetutorial import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview import org.koin.androidx.compose.koinViewModel import androidx.compose.material3.ExperimentalMaterial3Api @OptIn( ExperimentalMaterial3Api::class ) @Composable @Preview fun App() { val viewModel = koinViewModel<RocketLaunchViewModel>() val state by remember { viewModel.state } val coroutineScope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } val pullToRefreshState = rememberPullToRefreshState() }Здесь вы используете API ViewModel для Koin, чтобы обратиться к
viewModel, объявленному в модуле Koin для Android. -
Теперь добавьте код пользовательского интерфейса для экрана загрузки, столбца с результатами запусков и действия обновления с помощью свайпа вниз:
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.jetbrains.spacetutorial.entity.RocketLaunch import com.jetbrains.spacetutorial.theme.AppTheme import com.jetbrains.spacetutorial.theme.app_theme_successful import com.jetbrains.spacetutorial.theme.app_theme_unsuccessful import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @Preview fun App() { val viewModel = koinViewModel<RocketLaunchViewModel>() val state by remember { viewModel.state } val coroutineScope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } val pullToRefreshState = rememberPullToRefreshState() AppTheme { Scaffold( topBar = { TopAppBar( title = { Text( "Space Launches", style = MaterialTheme.typography.headlineLarge ) } ) } ) { padding -> PullToRefreshBox( modifier = Modifier .fillMaxSize() .padding(padding), state = pullToRefreshState, isRefreshing = isRefreshing, onRefresh = { isRefreshing = true coroutineScope.launch { viewModel.loadLaunches() isRefreshing = false } } ) { if (state.isLoading && !isRefreshing) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize() ) { Text("Loading...", style = MaterialTheme.typography.bodyLarge) } } else { LazyColumn { items(state.launches) { launch: RocketLaunch -> Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp) ) { Text( text = launch.missionName, style = MaterialTheme.typography.headlineSmall ) Text( text = if (launch.status.id == 3) "Successful" else "Unsuccessful", color = if (launch.status.id == 3) app_theme_successful else app_theme_unsuccessful ) Text( text = "Launch year: ${launch.launchYear}" ) val details = launch.status.description if (details.isNotBlank()) { Text(details) } } HorizontalDivider() } } } } } } } -
Наконец, в файле
androidApp/src/main/AndroidManifest.xmlукажите классMainActivityв теге<activity>:<manifest xmlns:android="http://schemas.android.com/apk/res/android"> ... <application ... <activity ... android:name="com.jetbrains.spacetutorial.MainActivity"> ... </activity> </application> </manifest> -
Запустите приложение Android: выберите androidApp в меню конфигураций запуска, выберите эмулятор и нажмите кнопку запуска. Приложение автоматически выполнит запрос к API и отобразит список запусков (цвет фона зависит от созданной вами темы Material):

Вы только что создали приложение Android, бизнес-логика которого реализована в модуле Kotlin Multiplatform, а пользовательский интерфейс работает на нативном Jetpack Compose.
Создание приложения iOS
Для части проекта, связанной с iOS, вы будете использовать SwiftUI для создания пользовательского интерфейса и паттерн Model View View-Model.
IntelliJ IDEA создает проект iOS, уже подключенный к общему модулю. Модуль Kotlin экспортируется под именем, указанным в файле sharedLogic/build.gradle.kts (baseName = "SharedLogic"), и импортируется с помощью обычной инструкции import: import SharedLogic.
Добавление флага динамической компоновки для SQLDelight
По умолчанию IntelliJ IDEA создает проекты, настроенные на статическую компоновку фреймворков iOS.
Чтобы использовать нативный драйвер SQLDelight в iOS, добавьте флаг динамической компоновки, который позволит инструментам Xcode найти системный бинарный файл SQLite:
В IntelliJ IDEA выберите Файл | Открыть проект в Xcode, чтобы открыть проект в Xcode.
В Xcode нажмите на имя проекта, чтобы открыть его настройки.
Перейдите на вкладку Настройки сборки, переключитесь там на список Все и найдите поле Другие флаги компоновщика.
Разверните поле, нажмите знак плюса рядом с полем Отладка и вставьте строку
-lsqlite3в поле Любая архитектура | Любой SDK.-
Повторите процесс для поля Другие флаги компоновщика | Релиз.
Вернитесь в IntelliJ IDEA.
Подготовка класса Koin для внедрения зависимостей в iOS
Чтобы использовать классы и функции Koin в коде Swift, создайте специальный класс KoinComponent и объявите модуль Koin для iOS.
Создайте файл
KoinHelper.ktв каталогеsharedLogic/src/iosMain/kotlin/com/jetbrains/spacetutorial.-
Добавьте класс
KoinHelper, который будет оберткой для классаSpaceSDKс ленивым внедрением Koin:package com.jetbrains.spacetutorial import org.koin.core.component.KoinComponent import com.jetbrains.spacetutorial.entity.RocketLaunch import org.koin.core.component.inject class KoinHelper : KoinComponent { private val sdk: SpaceSDK by inject<SpaceSDK>() suspend fun getLaunches(forceReload: Boolean): List<RocketLaunch> { return sdk.getLaunches(forceReload = forceReload) } } -
Под классом
KoinHelperдобавьте функциюinitKoin(), которую вы будете использовать в Swift для инициализации и запуска модуля Koin для iOS:import com.jetbrains.spacetutorial.cache.IOSDatabaseDriverFactory import com.jetbrains.spacetutorial.network.SpaceApi import org.koin.core.context.startKoin import org.koin.dsl.module fun initKoin() { startKoin { modules(module { single<SpaceApi> { SpaceApi() } single<SpaceSDK> { SpaceSDK( databaseDriverFactory = IOSDatabaseDriverFactory(), api = get() ) } }) } }
Теперь можно запустить модуль Koin в приложении iOS, чтобы использовать нативный драйвер базы данных с общим классом SpaceSDK.
Реализация пользовательского интерфейса
Сначала вы создадите представление SwiftUI RocketLaunchRow для отображения элемента списка. Оно будет основано на представлениях HStack и VStack. В структуре RocketLaunchRow будут расширения с полезными вспомогательными функциями для отображения данных.
В IntelliJ IDEA убедитесь, что выбрано представление ** Проект **.
Создайте новый файл Swift в папке
iosApp/iosApp, рядом сContentView.swift, и назовите егоRocketLaunchRow.-
Обновите файл
RocketLaunchRow.swift, добавив следующий код:import SwiftUI import SharedLogic struct RocketLaunchRow: View { var rocketLaunch: RocketLaunch var body: some View { HStack() { VStack(alignment: .leading, spacing: 10.0) { Text("\(rocketLaunch.missionName)") .font(.system(size: 18)) .bold() .fixedSize(horizontal: false, vertical: true) Text(launchText).foregroundColor(launchColor) Text("Launch year: \(String(rocketLaunch.launchYear))") Text("\(rocketLaunch.status.description_)") } Spacer() } } } extension RocketLaunchRow { private var launchText: String { let isSuccess = rocketLaunch.status.id == 3 return isSuccess ? "Successful" : "Unsuccessful" } private var launchColor: Color { let isSuccess = rocketLaunch.status.id == 3 return isSuccess ? Color.green : Color.red } }Список запусков будет отображаться в представлении
ContentView, которое уже включено в проект. -
В файле
ContentView.swiftдобавьте расширение классаContentViewс классомViewModel, который будет подготавливать данные и управлять ими:extension ContentView { enum LoadableLaunches { case loading case result([RocketLaunch]) case error(String) } @MainActor class ViewModel: ObservableObject { @Published var launches = LoadableLaunches.loading } }Модель представления (
ContentView.ViewModel) связана с представлением (ContentView) с помощью фреймворка Combine:Класс
ContentView.ViewModelобъявлен какObservableObject.Атрибут
@Publishedиспользуется для свойстваlaunches, поэтому модель представления будет отправлять сигналы при каждом изменении этого свойства.
Удалите структуру
ContentView_Previews: вы не будете реализовывать предварительный просмотр, который должен быть совместим с вашей моделью представления.-
Обновите тело класса
ContentView, чтобы отображать список запусков и добавить возможность повторной загрузки.Это основа пользовательского интерфейса: на следующем этапе руководства вы реализуете функцию
loadLaunches.Свойство
viewModelпомечено атрибутом@ObservedObjectдля подписки на модель представления.
struct ContentView: View { @ObservedObject private(set) var viewModel: ViewModel var body: some View { NavigationView { listView() .navigationBarTitle("Space Launches") .navigationBarItems(trailing: Button("Reload") { self.viewModel.loadLaunches(forceReload: true) }) } } private func listView() -> AnyView { switch viewModel.launches { case .loading: return AnyView(Text("Loading...").multilineTextAlignment(.center)) case .result(let launches): return AnyView(List(launches) { launch in RocketLaunchRow(rocketLaunch: launch) }) case .error(let description): return AnyView(Text(description).multilineTextAlignment(.center)) } } } -
Класс
RocketLaunchиспользуется в качестве параметра для инициализации представленияList, поэтому он должен соответствовать протоколуIdentifiable. В классе уже есть свойство с именемid, поэтому достаточно добавить расширение в конец файлаContentView.swift:extension RocketLaunch: Identifiable { }
Загрузка данных
Чтобы получить данные о запусках ракет в модели представления, вам понадобится экземпляр класса KoinHelper из библиотеки Multiplatform. Он позволит вызвать функцию SDK с подходящим драйвером базы данных.
-
В файле
ContentView.swiftдополните классViewModel, добавив объектKoinHelperи функциюloadLaunches:extension ContentView { // ... class ViewModel: ObservableObject { // ... let helper: KoinHelper = KoinHelper() init() { self.loadLaunches(forceReload: false) } func loadLaunches(forceReload: Bool) { // TODO: retrieve data } } } -
В функции
loadLaunches()вызовите функциюKoinHelper.getLaunches()(которая перенаправит вызов классуSpaceSDK) и сохраните результат в свойствеlaunches:func loadLaunches(forceReload: Bool) { Task { do { self.launches = .loading let launches = try await helper.getLaunches(forceReload: forceReload) self.launches = .result(launches) } catch { self.launches = .error(error.localizedDescription) } } }При компиляции модуля Kotlin в фреймворк Apple приостанавливаемые функции можно вызывать с помощью механизма
async/awaitв Swift.Поскольку функция
getLaunchesпомечена аннотацией@Throws(Exception::class)в Kotlin, любые исключения, являющиеся экземплярами классаExceptionили его подкласса, будут переданы в Swift какNSError. Поэтому все такие исключения можно перехватить функциейloadLaunches(). -
Перейдите к точке входа приложения — файлу
iOSApp.swift— и инициализируйте модуль Koin, представление и модель представления:import SwiftUI import SharedLogic @main struct iOSApp: App { init() { KoinHelperKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView(viewModel: .init()) } } } В IntelliJ IDEA переключитесь на конфигурацию iosApp, выберите эмулятор и запустите приложение, чтобы увидеть результат:

Что дальше?
В этом руководстве используются некоторые потенциально ресурсоемкие операции, например разбор JSON и выполнение запросов к базе данных в главном потоке. Чтобы узнать, как писать конкурентный код и оптимизировать приложение, ознакомьтесь с руководством по корутинам.
Также ознакомьтесь с дополнительными материалами:
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/multiplatform-ktor-sqldelight.html