Делитесь большей частью логики между iOS и Android
Теперь, когда вы реализовали общую логику с помощью внешних зависимостей, можно добавить более сложную логику. Сетевые запросы и сериализация данных — это самые популярные сценарии использования Kotlin Multiplatform для совместного использования кода. Узнайте, как реализовать их в первом приложении, чтобы использовать полученные знания в будущих проектах после завершения этого вводного курса.
Обновленное приложение будет получать данные через интернет из API LaunchLibrary 2 и отображать описания последних космических запусков.
Добавьте дополнительные зависимости
В проект нужно добавить следующие мультиплатформенные библиотеки:
kotlinx.coroutines— для использования корутин при выполнении параллельных операций.kotlinx.serialization— для десериализации JSON-ответов API SpaceX в объекты классов сущностей, используемые для обработки сетевых операций.Ktor — фреймворк для отправки и получения данных по HTTP.
Обновите каталог версий Gradle
Добавьте следующие записи в gradle/libs.versions.toml, а затем синхронизируйте файлы Gradle, чтобы эти ссылки стали доступны в коде конфигурации сборки:
[versions]
coroutinesVersion = "1.11.0"
ktorVersion = "3.5.2"
# A Kotlin version should already be set in the catalog
kotlin = "2.4.20"
[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorVersion" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorVersion" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktorVersion" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktorVersion" }
[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Добавьте зависимости в соответствующие наборы исходного кода
Добавьте ссылки на библиотеки в соответствующие наборы исходного кода в файле sharedLogic/build.gradle.kts:
plugins {
// ...
alias(libs.plugins.kotlinSerialization)
}
kotlin {
sourceSets {
commonMain.dependencies {
// ...
// The Kotlin Multiplatform Gradle plugin adds
// platform-specific coroutines artifacts automatically
implementation(libs.kotlinx.coroutines)
// Main Ktor dependency
implementation(libs.ktor.client.core)
// Dependencies that allow Ktor to use serialization
// with a specific format
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
}
androidMain.dependencies {
// Provides the Android engine for Ktor
implementation(libs.ktor.client.android)
}
iosMain.dependencies {
// Provides the Darwin engine for Ktor
implementation(libs.ktor.client.darwin)
}
}
}
Синхронизируйте файлы Gradle, нажав кнопку Синхронизировать изменения Gradle.
Настройте запросы к API
Для получения данных вы будете использовать Launch Library API, а именно список всех запусков из конечной точки /2.3.0/launches.
Создайте модель данных
В каталоге sharedLogic/src/commonMain/.../greetingkmp создайте новый файл RocketLaunch.kt и добавьте класс данных, который хранит данные из API SpaceX:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RocketLaunch(
@SerialName("id")
val id: String,
@SerialName("name")
val missionName: String,
@SerialName("net")
val launchDateUTC: String,
@SerialName("status")
val status: LaunchStatus,
)
@Serializable
data class LaunchStatus(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
)
@Serializable
data class LaunchListResponse(
@SerialName("results")
val results: List<RocketLaunch>,
)
Класс
RocketLaunchпомечен аннотацией@Serializable, чтобы плагинkotlinx.serializationмог автоматически создать для него сериализатор по умолчанию.Аннотация
@SerialNameпозволяет переопределить имена полей, чтобы объявлять свойства с более понятными именами в классах данных.
Подключите HTTP-клиент
В каталоге
sharedLogic/src/commonMain/.../greetingkmpсоздайте новый классRocketComponent.-
Добавьте свойство
httpClientдля получения информации о запусках ракет с помощью HTTP GET-запроса: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 RocketComponent { private val httpClient = HttpClient { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true }) } } }Плагин Ktor
ContentNegotiationи сериализатор JSON десериализуют результат GET-запроса.Здесь сериализатор JSON настроен так, чтобы выводить JSON в более удобном для чтения виде с помощью свойства
prettyPrint. Это обеспечивает большую гибкость при чтении некорректного JSON с помощьюisLenient, а также позволяет игнорировать ключи, не объявленные в модели запуска ракеты, с помощьюignoreUnknownKeys.
-
Добавьте в
RocketComponentприостанавливающую функциюgetDateOfLastSuccessfulLaunch(), которая будет асинхронно получать информацию о запусках ракет:import io.ktor.client.request.get import io.ktor.client.call.body class RocketComponent { // ... private suspend fun getDateOfLastSuccessfulLaunch(): String { val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body() // Initialized with a stub date for now val date: String = "October 5, 2026" return "$date" } }httpClient.get()также является приостанавливающей функцией, поскольку ей нужно асинхронно получать данные по сети, не блокируя потоки.Приостанавливающие функции можно вызывать только из корутин или других приостанавливающих функций. Поэтому
getDateOfLastSuccessfulLaunch()помечена ключевым словомsuspend. Сетевой запрос выполняется в пуле потоков HTTP-клиента.
-
После вызова HTTP-запроса добавьте вызов, который получает последний успешный запуск из списка (список запусков отсортирован по дате от самой ранней к самой поздней):
class RocketComponent { // ... private suspend fun getDateOfLastSuccessfulLaunch(): String { val response: LaunchListResponse = httpClient.get("https://lldev.thespacedevs.com/2.3.0/launches/previous/?mode=list&limit=10&format=json").body() val lastSuccessLaunch = response.results.first { it.status.id == 3 } val date: String = "October 5, 2026" return "$date" } } -
Преобразуйте дату и время запуска в формате UTC в местную дату и присвойте результат переменной
date. Затем верните отформатированный результат:import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlin.time.ExperimentalTime import kotlin.time.Instant class RocketComponent { // ... private suspend fun getDateOfLastSuccessfulLaunch(): String { val response: LaunchListResponse = httpClient.get("https://lldev.thespacedevs.com/2.3.0/launches/previous/?mode=list&limit=10&format=json").body() val lastSuccessLaunch = response.results.first { it.status.id == 3 } val date = Instant.parse(lastSuccessLaunch.launchDateUTC) .toLocalDateTime(TimeZone.currentSystemDefault()) return "${date.month} ${date.day}, ${date.year}" } }Дата будет отображаться в формате «MMMM DD, YYYY», например «OCTOBER 5, 2022».
-
Добавьте в этот же класс еще одну приостанавливающую функцию,
launchPhrase(), которая создаст сообщение с помощью функцииgetDateOfLastSuccessfulLaunch():class RocketComponent { // ... suspend fun launchPhrase(): String = try { "The last successful launch was on ${getDateOfLastSuccessfulLaunch()} 🚀" } catch (e: Exception) { println("Exception during getting the date of the last successful launch $e") "Error occurred" } }
Создайте поток корутин
Вместо простого вызова приостанавливающей функции можно использовать потоки, если нужно получить последовательность значений. Потоки могут выдавать значения по мере их появления, вместо того чтобы возвращать одно значение, как это делают приостанавливающие функции.
Откройте файл
Greeting.ktв каталогеshared/src/commonMain/kotlin.-
Добавьте свойство
rocketComponentв классGreeting. В этом свойстве будет храниться сообщение с датой последнего успешного запуска:class Greeting { private val rocketComponent = RocketComponent() //... } -
Измените функцию
greet(), чтобы она возвращалаFlow:import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlin.time.Duration.Companion.seconds class Greeting { // ... fun greet(): Flow<String> = flow { emit(if (Random.nextBoolean()) "Hi!" else "Hello!") delay(1.seconds) emit("Guess what this is! > ${platform.name.reversed()}") delay(1.seconds) emit(daysPhrase()) emit(rocketComponent.launchPhrase()) } }Здесь
Flowсоздается с помощью функции-конструктораflow(), которая оборачивает все операторы.Flowвыдает строки с интервалом в одну секунду. Последний элемент выдается только после получения ответа от сети, поэтому точная задержка зависит от вашего подключения.
Вы обновили API общего модуля, изменив тип возвращаемого значения функции greet() на Flow. Теперь нужно обновить нативные части проекта, чтобы они могли правильно обрабатывать результат вызова функции greet().
Обновите нативный интерфейс Android
Поскольку и общий модуль, и приложение для Android написаны на Kotlin, использовать общий код в Android просто.
Добавьте модель представления
Модели представления — популярный шаблон разработки Android, который помогает управлять данными и другими компонентами приложения, сохраняемыми в течение жизненного цикла Activity в Android. Теперь, когда приложение становится сложнее, пришло время добавить в него модель представления. Она будет хранить данные, полученные из API SpaceX, и предоставлять их интерфейсу.
Создайте класс модели представления в коде платформы Android:
-
В каталоге
sharedUI/src/commonMain/.../greetingkmpсоздайте новый класс KotlinMainViewModel:import androidx.lifecycle.ViewModel class MainViewModel : ViewModel() { // ... }Этот класс расширяет класс
ViewModelAndroid, чтобы соответствовать ожиданиям платформы относительно жизненного цикла и изменений конфигурации. -
Создайте значение
greetingListтипа StateFlow и его резервное свойство:import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class MainViewModel : ViewModel() { private val _greetingList = MutableStateFlow<List<String>>(listOf()) val greetingList: StateFlow<List<String>> get() = _greetingList }Здесь
StateFlowреализует интерфейсFlow, но содержит только одно значение, или состояние.Закрытое резервное свойство
_greetingListгарантирует, что клиенты этого класса могут обращаться только к свойствуgreetingList, доступному только для чтения.
-
В функции
initмодели представления соберите все строки из потокаGreeting().greet():import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class MainViewModel : ViewModel() { private val _greetingList = MutableStateFlow<List<String>>(listOf()) val greetingList: StateFlow<List<String>> get() = _greetingList init { viewModelScope.launch { Greeting().greet().collect { phrase -> //... } } } }Поскольку функция
Flow.collect()является приостанавливающей, в области видимости модели представления используется корутинаlaunch. Это означает, что корутина launch будет выполняться только на соответствующих этапах жизненного цикла модели представления. -
В завершающем лямбда-выражении
collectдобавьте собранное значениеphraseв список фраз в_greetingListс помощью функцииupdate():import kotlinx.coroutines.flow.update class MainViewModel : ViewModel() { //... init { viewModelScope.launch { Greeting().greet().collect { phrase -> _greetingList.update { list -> list + phrase } } } } }
Используйте поток модели представления
-
В
sharedUI/src/commonMain/.../greetingkmpоткройте файлApp.ktи обновите его, заменив предыдущую реализацию на новую модель представления:import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel @Composable @Preview fun App(mainViewModel: MainViewModel = viewModel()) { MaterialTheme { val greetings by mainViewModel.greetingList.collectAsStateWithLifecycle() Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { greetings.forEach { greeting -> Text(greeting) HorizontalDivider() } } } }Функция
collectAsStateWithLifecycle()вызываетgreetingList, чтобы получить значение из потока ViewModel и представить его в виде состояния Compose с учетом жизненного цикла.При создании нового потока состояние композиции изменится и отобразит прокручиваемый
Columnс расположенными вертикально приветственными фразами, разделенными линиями.
Добавьте разрешение на доступ к интернету
Для доступа к интернету приложению Android требуется соответствующее разрешение. Поскольку все сетевые запросы выполняются из общего модуля, имеет смысл добавить разрешение на доступ к интернету в его манифест.
Обновите файл 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>
Запустите приложение
Чтобы увидеть итоговый результат, повторно запустите конфигурацию запуска androidApp:
Обновите нативный интерфейс iOS
Для части проекта, предназначенной для iOS, вы используете шаблон модель — представление — модель представления, как и в приложении Android, чтобы связать интерфейс с модулем sharedLogic.
Модуль уже импортирован в файл ContentView.swift с помощью объявления import SharedLogic.
Добавьте ViewModel
В iosApp/ContentView.swift создайте класс ViewModel для ContentView, который будет подготавливать данные и управлять ими. Вызовите функцию startObserving() внутри вызова task() для поддержки конкурентного выполнения:
import SwiftUI
import SharedLogic
struct ContentView: View {
@ObservedObject private(set) var viewModel: ViewModel
var body: some View {
ListView(phrases: viewModel.greetings)
.task { await self.viewModel.startObserving() }
}
}
extension ContentView {
@MainActor
class ViewModel: ObservableObject {
@Published var greetings: Array<String> = []
func startObserving() {
// ...
}
}
}
struct ListView: View {
let phrases: Array<String>
var body: some View {
List(phrases, id: \.self) {
Text($0)
}
}
}
ViewModelобъявлена как расширениеContentView, поскольку они тесно связаны.ViewModelсодержит свойствоgreetings, представляющее собой массив фразString.
SwiftUI связывает модель представления (ContentView.ViewModel) с представлением (ContentView):
ContentView.ViewModelобъявлена какObservableObject. Обертка@ObservedObjectдля свойстваviewModelвContentViewподписывает представление на модель представления.В свойстве модели представления
greetingsиспользуется обертка@Published. Она позволяет SwiftUI автоматически обновлять представление при изменении этого свойства.
Теперь нужно реализовать функцию startObserving() для обработки потоков.
Выберите библиотеку для обработки потоков в iOS
В этом руководстве для работы с потоками в iOS можно использовать SKIE или библиотеку KMP-NativeCoroutines. Это решения с открытым исходным кодом, которые поддерживают отмену и обобщения для потоков, чего компилятор Kotlin/Native пока не предоставляет по умолчанию:
Библиотека KMP-NativeCoroutines помогает обрабатывать приостанавливающие функции и потоки в iOS, генерируя необходимые обертки. KMP-NativeCoroutines поддерживает функциональность Swift
async/await, а также Combine и RxSwift. Для использования KMP-NativeCoroutines в проекты iOS нужно добавить зависимость SwiftPM или CocoaPod.Библиотека SKIE расширяет API Objective-C, создаваемый компилятором Kotlin: SKIE преобразует потоки в аналог Swift
AsyncSequence. SKIE напрямую поддерживаетasync/awaitSwift без ограничений по потокам и с автоматической двусторонней отменой (для Combine и RxSwift требуются адаптеры). SKIE предлагает и другие возможности для создания удобного для Swift API на основе Kotlin, включая преобразование различных типов Kotlin в соответствующие типы Swift. Кроме того, для работы SKIE не требуется добавлять дополнительные зависимости в проекты iOS.
Вариант 1. Настройте KMP-NativeCoroutines
-
Добавьте версию KMP-NativeCoroutines и ссылку на плагин в каталог версий Gradle:
[versions] kmpNativeCoroutines = "1.0.5" [plugins] kmpNativeCoroutines = { id = "com.rickclephas.kmp.nativecoroutines", version.ref = "kmpNativeCoroutines" } -
В корневом файле
build.gradle.ktsпроекта (не в файлеshared/build.gradle.kts) добавьте плагин KMP-NativeCoroutines в блокplugins {}:plugins { // ... alias(libs.plugins.kmpNativeCoroutines) apply false } -
В файле
sharedLogic/build.gradle.ktsдобавьте плагин KMP-NativeCoroutines в блокplugins {}:plugins { // ... alias(libs.plugins.kmpNativeCoroutines) } -
В этом же файле
sharedLogic/build.gradle.ktsвключите экспериментальную аннотацию@ObjCName:kotlin { // ... sourceSets{ all { languageSettings { optIn("kotlin.experimental.ExperimentalObjCName") } } // ... } } Нажмите кнопку Синхронизировать изменения Gradle, чтобы синхронизировать файлы Gradle.
Пометьте поток с помощью KMP-NativeCoroutines
Откройте файл
Greeting.ktв каталогеsharedLogic/src/commonMain/kotlin.-
Добавьте аннотацию
@NativeCoroutinesк функцииgreet(). Это позволит плагину сгенерировать код, необходимый для правильной обработки потоков в iOS:import com.rickclephas.kmp.nativecoroutines.NativeCoroutines class Greeting { // ... @NativeCoroutines fun greet(): Flow<String> = flow { // ... } }
Импортируйте библиотеку с помощью SwiftPM в Xcode
Установите необходимые для работы с механизмом async/await компоненты пакета KMP-NativeCoroutines Swift.
Выберите File | Open Project in Xcode.
В Xcode щелкните правой кнопкой мыши проект
iosAppв меню слева и выберите Add Package Dependencies.-
В строке поиска введите имя пакета:
https://github.com/rickclephas/KMP-NativeCoroutines.git

В раскрывающемся списке Dependency Rule выберите пункт Exact Version и введите версию
1.0.5в соседнее поле.Нажмите кнопку Add Package. Xcode загрузит пакет с GitHub и откроет еще одно окно для выбора продуктов пакета.
-
Добавьте в приложение «KMPNativeCoroutinesAsync» и «KMPNativeCoroutinesCore», как показано ниже, затем нажмите Add Package:
Вернитесь в IntelliJ IDEA и выберите пункт меню Tools | Swift Package Manager | Resolve Dependencies. Будет создан файл блокировки
Package.resolved, используемый сборкой Kotlin. Его можно добавить в репозиторий, чтобы обеспечить единообразие версий пакетов Swift.
Обработайте поток с помощью библиотеки KMP-NativeCoroutines
-
В
iosApp/ContentView.swiftобновите функциюstartObserving(), чтобы обрабатывать поток с помощью функцииasyncSequence()из KMP-NativeCoroutines для функцииGreeting().greet():func startObserving() async { do { let sequence = asyncSequence(for: Greeting().greet()) for try await phrase in sequence { self.greetings.append(phrase) } } catch { print("Failed with error: \(error)") } }Здесь цикл и механизм
awaitиспользуются для перебора потока и обновления свойстваgreetingsпри каждом получении значения из потока. -
Убедитесь, что
ViewModelпомечена аннотацией@MainActor. Эта аннотация гарантирует, что все асинхронные операции внутриViewModelвыполняются в главном потоке, как того требует Kotlin/Native:// ... import KMPNativeCoroutinesAsync import KMPNativeCoroutinesCore // ... extension ContentView { @MainActor class ViewModel: ObservableObject { @Published var greetings: Array<String> = [] func startObserving() async { do { let sequence = asyncSequence(for: Greeting().greet()) for try await phrase in sequence { self.greetings.append(phrase) } } catch { print("Failed with error: \(error)") } } } }
Вариант 2. Настройте SKIE
Чтобы настроить библиотеку, добавьте версию SKIE и ссылку на плагин в каталог версий Gradle:
[versions]
skie = "0.10.14"
[plugins]
skie = { id = "co.touchlab.skie", version.ref = "skie" }
Затем добавьте его в список плагинов в файле sharedLogic/build.gradle.kts и нажмите кнопку Синхронизировать изменения Gradle:
plugins {
//...
alias(libs.plugins.skie)
}
Обработайте поток с помощью SKIE
Вы будете использовать цикл и механизм await для перебора потока Greeting().greet() и обновления свойства greetings при каждом получении значения из потока.
Убедитесь, что ViewModel помечена аннотацией @MainActor. Эта аннотация гарантирует, что все асинхронные операции внутри ViewModel выполняются в главном потоке, как того требует Kotlin/Native:
// ...
extension ContentView {
@MainActor
class ViewModel: ObservableObject {
@Published var greetings: [String] = []
func startObserving() async {
for await phrase in Greeting().greet() {
self.greetings.append(phrase)
}
}
}
}
Используйте ViewModel и запустите приложение iOS
В iosApp/iOSApp.swift обновите точку входа приложения:
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ContentView.ViewModel())
}
}
}
Запустите конфигурацию iosApp из IntelliJ IDEA, чтобы убедиться, что логика приложения синхронизирована:

Следующий шаг
В заключительной части руководства вы завершите работу над проектом и узнаете, что делать дальше.
См. также
Изучите различные подходы к композиции приостанавливающих функций.
Узнайте больше о взаимодействии с фреймворками и библиотеками Objective-C.
Пройдите руководство по сетевому взаимодействию и хранению данных.
Получите помощь
Kotlin Slack. Получите приглашение и присоединитесь к каналу #multiplatform.
Система отслеживания ошибок Kotlin. Сообщите о новой проблеме.
© 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-upgrade-app.html