Kotlin Multiplatform 2026: спільний код Android та iOS

Як Kotlin Multiplatform у 2026 році робить бізнес-логіку спільною між Android та iOS: налаштування Gradle, expect/actual, мережа Ktor, інтерфейс Compose Multiplatform і питання співбесіди з KMP.

Kotlin Multiplatform 2026 спільний код між застосунками Android та iOS

Kotlin Multiplatform (KMP) дозволяє одній кодовій базі на Kotlin забезпечувати бізнес-логіку застосунків для Android та iOS, тоді як кожна платформа зберігає повністю нативний інтерфейс, продуктивність і доступ до власного SDK. У 2026 році KMP уже не експеримент: він стабільний, офіційно підтримується Google і працює у продакшені в Netflix, McDonald's, Forbes та Cash App. Цей докладний огляд пояснює, як насправді працює спільне використання коду, як налаштувати спільний модуль і які компроміси варто зрозуміти перед співбесідою з KMP.

Що саме KMP робить спільним

Kotlin Multiplatform робить спільною скомпільовану логіку, а не середовище виконання. Спільний код Kotlin компілюється у байткод JVM для Android і в нативний бінарний файл (через LLVM) для iOS. Немає мосту JavaScript, вбудованого webview чи шару рефлексії, тому спільний код працює з нативною швидкістю на обох платформах.

Як Kotlin Multiplatform робить код спільним без втрати нативної продуктивності

Основний механізм, це ієрархія наборів джерел (source sets). commonMain містить незалежний від платформи код Kotlin: мережу, серіалізацію, бізнес-правила та моделі подання. Платформні набори джерел, такі як androidMain та iosMain, містять код, що звертається до API платформи. Компілятор створює артефакт JVM з commonMain плюс androidMain для Android і нативний фреймворк з commonMain плюс iosMain для iOS.

У 2026 році Kotlin Multiplatform віддає перевагу спільному використанню того, що справді виграє від єдиного джерела правди: моделей, клієнтів API, кешування та валідації. Інтерфейс можна зробити спільним за допомогою Compose Multiplatform або залишити повністю нативним зі SwiftUI, екран за екраном. Ця модель "спільна логіка, вибір інтерфейсу" є причиною того, чому команди впроваджують KMP без переписування наявних застосунків.

Kotlin Multiplatform, це технологія Kotlin, яка компілює один спільний модуль у кілька цілей (Android на JVM, iOS як нативний, а також десктоп і веб), тож бізнес-логіка пишеться один раз, а кожна платформа зберігає свій нативний інтерфейс і повний доступ до SDK. Google тепер підтримує його як варіант першого класу, а бібліотеки Jetpack, такі як Room, DataStore, ViewModel і Paging, пропонують артефакти KMP.

Рішення про те, що належить до спільного модуля, є найважливішим архітектурним вибором у проєкті KMP. Наведена нижче таблиця відображає, як більшість продакшн-команд проводять цю межу у 2026 році: усе, що нижче за шар подання, є сильним кандидатом на спільне використання, а все, що стосується UX-конвенцій платформи, зазвичай залишається нативним.

| Шар | Спільний у commonMain? | Причина | |-------|---------------------|--------| | Моделі даних і DTO | Так | Ідентичний контракт API на обох платформах | | Мережа і кешування | Так | Один стек HTTP, одна поверхня помилок | | Бізнес-правила і валідація | Так | Домен не відрізняється між ОС | | Моделі подання і стан | Зазвичай | Compose і SwiftUI споживають спільний стан | | Рендеринг інтерфейсу | Опціонально | Спільний з Compose або нативний на кожному екрані | | API платформи (камера, біометрія) | Ні | Натомість надається через expect/actual |

Вигода зростає залежно від того, якою частиною нижньої половини цієї таблиці володіє застосунок. Продукти з великою кількістю даних і тонким інтерфейсом роблять спільним найбільше; вузькоспеціалізовані, орієнтовані на анімацію застосунки, найменше. Жодна з крайнощів не є хибною, тому KMP підходить для поступового впровадження, а не для переписування всього одразу.

Налаштування спільного модуля у Gradle

Модуль KMP оголошує свої цілі та набори джерел у Kotlin DSL. Наведений нижче build.gradle.kts налаштовує Android і три архітектури iOS (фізичний пристрій, симулятор Intel, симулятор Apple Silicon) і надає вихідні дані iOS як фреймворк з назвою Shared.

shared/build.gradle.ktskotlin
plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization")
    id("com.android.library")
}

kotlin {
    // Android target compiles commonMain + androidMain to JVM bytecode
    androidTarget()

    // Three iOS targets: device, Intel sim, Apple Silicon sim
    listOf(iosArm64(), iosX64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"   // becomes "import Shared" in Swift
            isStatic = true
        }
    }

    sourceSets {
        commonMain.dependencies {
            implementation("io.ktor:ktor-client-core:3.3.0")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
        }
        androidMain.dependencies {
            implementation("io.ktor:ktor-client-okhttp:3.3.0")
        }
        iosMain.dependencies {
            implementation("io.ktor:ktor-client-darwin:3.3.0")
        }
    }
}

Кожна ціль підтягує специфічний для платформи рушій Ktor (okhttp на Android, darwin на iOS), тоді як спільний код залежить лише від загального API ktor-client-core. Повний набір параметрів наведено в документації Kotlin Multiplatform.

Написання специфічного для платформи коду за допомогою expect і actual

Частина логіки не може перебувати в commonMain, оскільки їй потрібне API платформи. Механізм expect/actual оголошує форму у спільному коді та надає конкретну реалізацію для кожної цілі, що вирішується під час компіляції без жодних витрат під час виконання.

commonMain/kotlin/Platform.ktkotlin
// commonMain declares what it needs but cannot implement here
expect class Platform() {
    val name: String
    val osVersion: String
}

// Shared logic can now depend on Platform freely
fun analyticsUserAgent(): String {
    val platform = Platform()
    return "SharpSkill/" + platform.name + " " + platform.osVersion
}

Реалізація для Android читає дані з класу Build, а реалізація для iOS, з UIDevice. Обидві задовольняють той самий контракт, тож commonMain ніколи не знає, з якою з них взаємодіє.

androidMain/kotlin/Platform.ktkotlin
import android.os.Build

// actual supplies the Android-specific implementation
actual class Platform actual constructor() {
    actual val name: String = "Android"
    actual val osVersion: String = Build.VERSION.RELEASE
}
iosMain/kotlin/Platform.ktkotlin
import platform.UIKit.UIDevice

// actual uses UIKit directly, no Objective-C bridging code required
actual class Platform actual constructor() {
    actual val name: String = "iOS"
    actual val osVersion: String = UIDevice.currentDevice.systemVersion
}

Kotlin/Native постачає типізовані прив'язки (bindings) для всього SDK iOS, тож типи UIDevice, NSUserDefaults і Foundation можна викликати з Kotlin без написаного вручну сполучного коду.

Спільне використання логіки мережі та серіалізації

Мережа, це код із найвищою цінністю для спільного використання, оскільки контракти API ідентичні на обох платформах. Один клієнт Ktor разом із kotlinx.serialization замінює дві паралельні реалізації. Функції suspend спираються на корутини Kotlin, які чисто відображаються на async/await з обох боків.

commonMain/kotlin/data/InterviewRepository.ktkotlin
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable

// One serializable model, zero duplication across platforms
@Serializable
data class Question(val id: String, val prompt: String, val difficulty: String)

// One HttpClient, one JSON parser, one repository for both apps
class InterviewRepository {
    private val client = HttpClient {
        install(ContentNegotiation) { json() }
    }

    suspend fun fetchQuestions(tech: String): List<Question> =
        client.get("https://api.sharpskill.dev/questions/" + tech).body()
}

Це репозиторій, його логіка повторних спроб і моделі даних написані один раз. Помилка, виправлена в парсері, виправляється на Android та iOS одночасно, і саме тут KMP окупає вартість налаштування. Той самий шаблон поширюється на спільну базу даних із SQLDelight, спільне сховище ключ-значення та спільне впровадження залежностей із Koin, тож увесь шар даних може перебувати в commonMain за інтерфейсами, від яких залежить інтерфейс користувача.

Поширена помилка, це намагання зробити спільним занадто багато й занадто рано. Прагматичний шлях, почати з одного репозиторію, перевірити збірку та конвеєр CI на обох платформах, а потім переносити функції по черзі, у міру зростання впевненості.

Готовий до співбесід з Android?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Побудова спільного інтерфейсу за допомогою Compose Multiplatform

Compose Multiplatform досяг стабільного стану на iOS у травні 2025 року (версія 1.8.0) і до середини 2026 року досяг версії 1.11.0 з нативним введенням тексту та ввімкненим за замовчуванням паралельним рендерингом. Функція @Composable у commonMain рендериться на обох платформах, а спільні моделі подання природно поєднуються з архітектурою MVVM або MVI.

commonMain/kotlin/ui/QuestionList.ktkotlin
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

// This list renders identically on Android and iOS
@Composable
fun QuestionList(questions: List<Question>) {
    LazyColumn {
        items(questions) { question ->
            Card(modifier = Modifier.padding(8.dp)) {
                Text(text = question.prompt, modifier = Modifier.padding(16.dp))
            }
        }
    }
}

Спільне використання інтерфейсу є опціональним і виконується екран за екраном. Команда може зробити спільним увесь інтерфейс за допомогою Compose Multiplatform, залишити SwiftUI для всього застосунку й робити спільною лише логіку, або поєднувати обидва підходи. Екрани, що залежать від специфічного для платформи UX, як-от віджети чи App Clips, зазвичай залишаються нативними.

Використання спільного коду зі Swift на iOS

Спільний модуль компілюється у фреймворк, який Swift імпортує як будь-яку бібліотеку. Історично тертя спричиняли функції suspend Kotlin і Flow, що з'являлися як незручні API на основі зворотних викликів. SKIE від Touchlab вирішує це, генеруючи ідіоматичний Swift: suspend стає нативним async/await, а Flow стає AsyncSequence.

iosApp/ContentView.swiftswift
import SwiftUI
import Shared   // the KMP framework, baseName = "Shared"

struct ContentView: View {
    @State private var questions: [Question] = []
    private let repository = InterviewRepository()

    var body: some View {
        List(questions, id: \.id) { question in
            Text(question.prompt)
        }
        .task {
            // With SKIE, the Kotlin suspend fun is a Swift async fun
            questions = (try? await repository.fetchQuestions(tech: "android")) ?? []
        }
    }
}

Команда iOS пише звичайний Swift і SwiftUI. Вона використовує спільні моделі та репозиторії точно так само, як використовувала б нативний пакет Swift, що робить KMP невидимим для більшої частини коду iOS.

Дві шорсткості все ще варто назвати на співбесіді. По-перше, час збірки: компіляція Kotlin/Native повільніша за чисту збірку Swift, хоча кеш збірки Gradle і розповсюдження попередньо зібраного XCFramework пом'якшують щоденні витрати. По-друге, налагодження через межу Swift-Kotlin покращується, але ще не таке безшовне, як робота в одній мові, що є ще однією причиною, чому команди тримають спільну поверхню свідомо обмеженою, а не розлогою.

Питання співбесіди з Kotlin Multiplatform, яких варто очікувати

Оскільки питання співбесіди з KMP дедалі частіше з'являються в ролях, пов'язаних з Android і мобільними застосунками, кілька повторюваних варто відрепетирувати поряд із ширшою підготовкою до співбесіди з Android:

  • Чим expect/actual відрізняється від інтерфейсу? expect/actual вирішується під час компіляції для кожної цілі без динамічного диспетчингу під час виконання й може забезпечувати клас, функцію, властивість або typealias. Інтерфейс вирішується динамічно під час виконання. expect/actual використовують для API платформи, а інтерфейси, для поліморфізму всередині спільного коду.
  • Як Kotlin/Native працює з пам'яттю та потоками? Починаючи з Kotlin 1.7.20 новий менеджер пам'яті прибрав стару модель заморожування об'єктів, тож спільний змінний стан і корутини працюють між потоками приблизно так само, як на JVM.
  • Чи може наявний застосунок впроваджувати KMP поступово? Так. Спільний модуль постачається як AAR для Android і XCFramework для iOS, тож один репозиторій або функція можуть мігрувати по черзі без переписування.
  • Коли інтерфейс має залишатися нативним замість використання Compose Multiplatform? Коли екран спирається на специфічний для платформи UX або коли команда iOS володіє шаром подання й воліє контролювати його.
Набір інструментів у 2026

Варто орієнтуватися на Kotlin 2.3 з компілятором K2, Ktor 3.x для мережі, SQLDelight для спільної бази даних і Koin для впровадження залежностей. Для взаємодії з iOS SKIE слід додати рано; додавати його після написання шару Swift значно болючіше.

Висновок

Kotlin Multiplatform у 2026 році, це прагматичний спосіб скоротити дубльовану логіку між Android та iOS, не відмовляючись від нативного інтерфейсу чи продуктивності:

  • Робіть спільною логіку, яка має єдине джерело правди (моделі, мережа, валідація), і обирайте нативний або Compose інтерфейс на кожному екрані.
  • Використовуйте expect/actual для API платформи; воно вирішується під час компіляції без накладних витрат під час виконання.
  • Пишіть мережу один раз за допомогою Ktor і kotlinx.serialization, щоб виправлення потрапляли на обидві платформи одночасно.
  • Впроваджуйте поступово через AAR і XCFramework, а не наважуйтеся на повне переписування.
  • Додайте SKIE від самого початку, щоб Swift споживав suspend і Flow як нативні async/await та AsyncSequence.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#Kotlin Multiplatform
#KMP
#Android
#iOS
#Cross-platform

Поділитися

Пов'язані статті