Как сделать Android-приложение совместимым с iOS — руководство
В этом руководстве показано, как сделать существующее Android-приложение кроссплатформенным, чтобы оно работало и на Android, и на iOS. Вы сможете одновременно писать код для Android и iOS в одном месте.
В этом руководстве используется пример Android-приложения с одним экраном для ввода имени пользователя и пароля. Учетные данные проверяются и сохраняются в базе данных в памяти.
Чтобы приложение работало и на iOS, и на Android, сначала сделайте код кроссплатформенным, переместив его часть в общий модуль. Затем используйте кроссплатформенный код в Android-приложении, а после этого — тот же код в новом iOS-приложении.
Подготовьте среду разработки
-
В кратком руководстве выполните инструкции по настройке среды для разработки на Kotlin Multiplatform.
-
В Android Studio создайте новый проект из системы контроля версий:
https://github.com/Kotlin/kmp-integration-sample
В ветке
masterнаходится исходное состояние проекта — простое Android-приложение. Чтобы увидеть итоговое состояние с iOS-приложением и общим модулем, переключитесь на веткуfinal. -
Переключитесь в представление Проект:

Сделайте код кроссплатформенным
Чтобы сделать код кроссплатформенным, выполните следующие действия:
Решите, какой код сделать кроссплатформенным
Решите, какой код Android-приложения лучше использовать совместно с iOS, а какой оставить нативным. Простое правило: делитесь тем, что хотите использовать повторно как можно чаще. Бизнес-логика часто одинакова для Android и iOS, поэтому она отлично подходит для повторного использования.
В примере Android-приложения бизнес-логика хранится в пакете com.jetbrains.simplelogin.androidapp.data. Будущее iOS-приложение будет использовать ту же логику, поэтому ее тоже следует сделать кроссплатформенной.
Создайте общий модуль для кроссплатформенного кода
Кроссплатформенный код, используемый и на iOS, и на Android, будет храниться в общем модуле. В Android Studio и IntelliJ IDEA есть мастер создания общих модулей для Kotlin Multiplatform.
Создайте общий модуль, который будет связан с существующим Android-приложением и будущим iOS-приложением:
В Android Studio выберите в главном меню Файл | Создать | Создать модуль.
-
В списке шаблонов выберите Общий модуль Kotlin Multiplatform. Оставьте имя модуля
sharedи укажите имя пакета:com.jetbrains.simplelogin.shared
Нажмите Готово. Мастер создаст общий модуль, соответствующим образом изменит скрипт сборки и запустит синхронизацию Gradle.
-
Дождитесь завершения синхронизации. В каталоге
sharedпоявится следующая структура файлов:
Чтобы лучше понять структуру получившегося проекта, ознакомьтесь с основами структуры проекта Kotlin Multiplatform.
-
Замените блок
kotlin.android {}в файлеshared/build.gradle.ktsследующим блокомandroidLibrary {}, поскольку модульsharedбудет использоваться как библиотека Android-приложения:import org.jetbrains.kotlin.gradle.dsl.JvmTarget kotlin { androidLibrary { namespace = "com.jetbrains.simplelogin.shared" compileSdk = libs.versions.android.compileSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } androidResources { enable = true } withHostTestBuilder { } withDeviceTestBuilder { sourceSetTreeName = "test" }.configure { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } //... }
Добавьте код в общий модуль
Теперь, когда у вас есть общий модуль, добавьте общий код в каталог shared/src/commonMain/kotlin/com.jetbrains.simplelogin.shared:
-
Создайте класс
Greetingсо следующим кодом:package com.jetbrains.simplelogin.shared class Greeting { private val platform = getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } } -
Замените код в существующих файлах следующим:
-
В
commonMain/Platform.kt:package com.jetbrains.simplelogin.shared interface Platform { val name: String } expect fun getPlatform(): Platform -
В
androidMain/Platform.android.kt:package com.jetbrains.simplelogin.shared import android.os.Build class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" } actual fun getPlatform(): Platform = AndroidPlatform() -
В
iosMain/Platform.ios.kt:package com.jetbrains.simplelogin.shared import platform.UIKit.UIDevice class IOSPlatform: Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } actual fun getPlatform(): Platform = IOSPlatform()
-
Теперь у вас есть общая функция getPlatform(), которая возвращает специфичный для платформы объект со свойством, содержащим название платформы.
Добавьте зависимость от общего модуля в Android-приложение
Чтобы использовать кроссплатформенный код в Android-приложении, подключите к нему общий модуль, переместите туда код бизнес-логики и сделайте этот код кроссплатформенным.
-
Добавьте зависимость от общего модуля в файл
app/build.gradle.kts:dependencies { // ... implementation(project(":shared")) } Синхронизируйте файлы Gradle, следуя предложению IDE или выбрав пункт меню Файл | Синхронизировать проект с файлами Gradle.
В каталоге
app/src/main/java/откройте файлLoginActivity.ktв пакетеcom.jetbrains.simplelogin.androidapp.ui.login.-
Чтобы убедиться, что общий модуль успешно подключен к приложению, запишите результат функции
greet()в журнал, добавив вызовLog.i()в методonCreate():override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) Log.i("Login Activity", "Hello from shared module: " + (Greeting().greet())) // ... } Следуйте рекомендациям IDE, чтобы импортировать недостающие классы.
-
На панели инструментов нажмите значок отладки рядом с раскрывающимся списком конфигураций запуска:
-
В окне инструментов Logcat найдите в журнале строку «Hello» — вы увидите приветствие из общего модуля:
Сделайте бизнес-логику кроссплатформенной
Теперь можно извлечь код бизнес-логики в набор исходных файлов commonMain общего модуля Kotlin Multiplatform. Это позволит использовать код и на Android, и на iOS.
-
Переместите код бизнес-логики
com.jetbrains.simplelogin.androidapp.dataиз каталогаappв пакетcom.jetbrains.simplelogin.sharedв каталогеshared/src/commonMain.
-
Когда Android Studio спросит, что нужно сделать, выберите перемещение пакета и подтвердите рефакторинг.
-
Игнорируйте все предупреждения о зависимом от платформы коде и нажмите Все равно выполнить рефакторинг.

-
Удалите специфичный для Android код, заменив его кроссплатформенным кодом Kotlin или подключившись к специфичным для Android API с помощью объявлений expected и actual. Подробности см. в следующих разделах:
Замените специфичный для Android код кроссплатформенным кодом
Чтобы код корректно работал и на Android, и на iOS, по возможности замените все зависимости JVM на зависимости Kotlin в перемещенном каталоге
data.-
В классе
LoginDataValidatorзамените классPatternsиз пакетаandroid.utilsрегулярным выражением Kotlin, которое соответствует шаблону проверки адреса электронной почты:// Before private fun isEmailValid(email: String) = Patterns.EMAIL_ADDRESS.matcher(email).matches()
// After private fun isEmailValid(email: String) = emailRegex.matches(email) companion object { private val emailRegex = ("[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+").toRegex() } -
Удалите директиву импорта класса
Patterns:import android.util.Patterns
-
В классе
LoginDataSourceзаменитеIOExceptionв функцииlogin()наRuntimeException.IOExceptionнедоступен в Kotlin/JVM.```kotlin // Before return Result.Error(IOException("Error logging in", e)) ``` ```kotlin // After return Result.Error(RuntimeException("Error logging in", e)) ``` -
Также удалите директиву импорта для
IOException:import java.io.IOException
Реализуйте генерацию UUID с учетом платформы
В классе
LoginDataSourceуникальный идентификатор (UUID) дляfakeUserсоздается с помощью классаjava.util.UUID, недоступного в iOS.val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
Несмотря на то, что стандартная библиотека Kotlin предоставляет класс для генерации UUID, в этом примере воспользуемся специфичными для платформы средствами, чтобы попрактиковаться в таком подходе.
Добавьте объявление
expectдля функцииrandomUUID()в общий код и ее реализацииactualдля каждой платформы — Android и iOS — в соответствующие наборы исходных файлов. Подробнее о том, как подключаться к специфичным для платформы API.-
Замените вызов
java.util.UUID.randomUUID()в функцииlogin()вызовомrandomUUID(), который вы реализуете для каждой платформы:val fakeUser = LoggedInUser(randomUUID(), "Jane Doe")
-
Создайте файл
Utils.ktв пакетеcom.jetbrains.simplelogin.sharedкаталогаshared/src/commonMainи добавьте объявлениеexpect:package com.jetbrains.simplelogin.shared expect fun randomUUID(): String
-
Создайте файл
Utils.android.ktв пакетеcom.jetbrains.simplelogin.sharedкаталогаshared/src/androidMainи добавьте реализациюactualдляrandomUUID()на Android:package com.jetbrains.simplelogin.shared import java.util.* actual fun randomUUID() = UUID.randomUUID().toString()
-
Создайте файл
Utils.ios.ktв каталогеcom.jetbrains.simplelogin.sharedдиректорииshared/src/iosMainи добавьте реализациюactualдляrandomUUID()на iOS:package com.jetbrains.simplelogin.shared import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString()
-
Импортируйте функцию
randomUUIDв файлLoginDataSource.ktкаталогаshared/src/commonMain:import com.jetbrains.simplelogin.shared.randomUUID
-
Теперь Kotlin будет использовать специфичные для платформы реализации UUID для Android и iOS.
Запустите кроссплатформенное приложение на Android
Запустите конфигурацию запуска app, чтобы убедиться, что Android-приложение работает как прежде.
Сделайте кроссплатформенное приложение совместимым с iOS
После того как вы сделаете Android-приложение кроссплатформенным, можно создать iOS-приложение и повторно использовать в нем общую бизнес-логику.
Создайте iOS-проект в Xcode
В Xcode нажмите Файл | Создать | Проект.
-
В диалоговом окне перейдите на вкладку iOS:
Выберите шаблон Приложение, затем нажмите Далее.
-
Укажите в качестве имени продукта «simpleLoginIOS» и нажмите Далее.
-
В качестве расположения проекта выберите каталог, в котором хранится кроссплатформенное приложение, например
kmp-integration-sample.В Android Studio вы увидите следующую структуру:
-
Для единообразия с другими каталогами верхнего уровня кроссплатформенного проекта закройте Xcode и переименуйте каталог
simpleLoginIOSвiosApp.
Настройте iOS-проект для использования фреймворка KMP
Можно настроить интеграцию между iOS-приложением и фреймворком, собранным Kotlin Multiplatform.
В Android Studio щелкните правой кнопкой мыши каталог
iosApp/simpleLoginIOS.xcodeprojи выберите Открыть в | Открыть в связанном приложении, чтобы открыть iOS-проект в Xcode.В Xcode нажмите имя проекта в навигаторе Проект, чтобы открыть настройки iOS-проекта.
В разделе Цели слева выберите simpleLoginIOS, затем откройте вкладку Этапы сборки.
-
Нажмите значок + и выберите Новый этап Run Script.

-
Вставьте следующий скрипт в поле скрипта запуска:
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" exit 0 fi cd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode -
Отключите параметр На основе анализа зависимостей. Это гарантирует, что Xcode будет запускать скрипт при каждой сборке и не будет каждый раз предупреждать об отсутствующих зависимостях выходных данных.

-
Переместите этап Run Script выше — перед этапом Компиляция исходных файлов:
-
На вкладке Параметры сборки отключите параметр Песочница пользовательских скриптов в разделе Параметры сборки:
На вкладке Информация добавьте пользовательское свойство
CADisableMinimumFrameDurationOnPhoneи задайте для него значениеYES, чтобы включить высокую частоту обновления экрана в iOS.-
На вкладке Подписание и возможности выберите команду разработки или создайте ее, если еще не сделали этого. Это позволит подписать фреймворк
shared, созданный модулем KMP.Здесь также убедитесь, что для параметра Идентификатор пакета задано уникальное значение, иначе сборка в Xcode может завершиться с ошибкой.
-
Соберите проект в Xcode (в главном меню выберите Продукт | Собрать). Если все настроено правильно, сборка проекта завершится успешно (предупреждение «этап сборки будет выполняться при каждой сборке» можно безопасно проигнорировать).
Настройте конфигурацию запуска iOS в Android Studio
Убедившись, что Xcode настроен правильно, вернитесь в Android Studio:
-
В главном меню выберите Файл | Синхронизировать проект с файлами Gradle. Android Studio автоматически создаст конфигурацию запуска simpleLoginIOS.
Android Studio автоматически создаст конфигурацию запуска simpleLoginIOS и отметит каталог
iosAppкак связанный проект Xcode. -
В списке конфигураций запуска выберите simpleLoginIOS. Выберите эмулятор iOS, затем нажмите Запустить, чтобы проверить работу конфигурации запуска iOS.
Используйте общий модуль в iOS-проекте
Файл shared/build.gradle.kts задает свойство binaries.framework.baseName для каждой цели iOS со значением sharedKit. Это имя фреймворка, который Kotlin Multiplatform собирает для использования в iOS-приложении.
Чтобы проверить интеграцию, добавьте вызов общего кода в код Swift:
-
В Android Studio откройте файл
iosApp/simpleloginIOS/ContentView.swiftи импортируйте фреймворк:import sharedKit
-
Чтобы убедиться, что подключение выполнено правильно, обновите код структуры
ContentView, чтобы использовать функциюgreet()из модуляshared:struct ContentView: View { var body: some View { Text(Greeting().greet()) .padding() } } -
Запустите приложение с помощью конфигурации запуска iOS в Android Studio, чтобы увидеть результат:

-
Снова обновите код в файле
ContentView.swift, чтобы использовать бизнес-логику из общего модуля для отображения интерфейса приложения:import SwiftUI import Combine import sharedKit struct ContentView: View { @State private var username: String = "" @State private var password: String = "" @ObservedObject var viewModel: ContentView.ViewModel var body: some View { VStack(spacing: 15.0) { ValidatedTextField(titleKey: "Username", secured: false, text: $username, errorMessage: viewModel.formState.usernameError, onChange: { viewModel.loginDataChanged(username: username, password: password) }) ValidatedTextField(titleKey: "Password", secured: true, text: $password, errorMessage: viewModel.formState.passwordError, onChange: { viewModel.loginDataChanged(username: username, password: password) }) Button("Login") { viewModel.login(username: username, password: password) }.disabled(!viewModel.formState.isDataValid || (username.isEmpty && password.isEmpty)) } .padding(.all) } } struct ValidatedTextField: View { let titleKey: String let secured: Bool @Binding var text: String let errorMessage: String? let onChange: () -> () @ViewBuilder var textField: some View { if secured { SecureField(titleKey, text: $text) } else { TextField(titleKey, text: $text) } } var body: some View { ZStack { textField .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .onChange(of: text) { _ in onChange() } if let errorMessage = errorMessage { HStack { Spacer() FieldTextErrorHint(error: errorMessage) }.padding(.horizontal, 5) } } } } struct FieldTextErrorHint: View { let error: String @State private var showingAlert = false var body: some View { Button(action: { self.showingAlert = true }) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) } .alert(isPresented: $showingAlert) { Alert(title: Text("Error"), message: Text(error), dismissButton: .default(Text("Got it!"))) } } } extension ContentView { struct LoginFormState { let usernameError: String? let passwordError: String? var isDataValid: Bool { get { return usernameError == nil && passwordError == nil } } } class ViewModel: ObservableObject { @Published var formState = LoginFormState(usernameError: nil, passwordError: nil) let loginValidator: LoginDataValidator let loginRepository: LoginRepository init(loginRepository: LoginRepository, loginValidator: LoginDataValidator) { self.loginRepository = loginRepository self.loginValidator = loginValidator } func login(username: String, password: String) { if let result = loginRepository.login(username: username, password: password) as? ResultSuccess { print("Successful login. Welcome, \(result.data.displayName)") } else { print("Error while logging in") } } func loginDataChanged(username: String, password: String) { formState = LoginFormState( usernameError: (loginValidator.checkUsername(username: username) as? LoginDataValidator.ResultError)?.message, passwordError: (loginValidator.checkPassword(password: password) as? LoginDataValidator.ResultError)?.message) } } } -
В файле
simpleLoginIOSApp.swiftимпортируйте модульsharedKitи укажите аргументы для функцииContentView():import SwiftUI import sharedKit @main struct SimpleLoginIOSApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: .init(loginRepository: LoginRepository(dataSource: LoginDataSource()), loginValidator: LoginDataValidator())) } } } Еще раз запустите конфигурацию запуска iOS, чтобы убедиться, что в iOS-приложении отображается форма входа.
Введите «Jane» в качестве имени пользователя и «password» в качестве пароля.
-
Поскольку вы настроили интеграцию ранее, iOS-приложение проверяет введенные данные с помощью общего кода:

Наслаждайтесь результатами — обновляйте логику только один раз
Теперь ваше приложение работает на нескольких платформах. Вы можете обновить бизнес-логику в модуле shared и увидеть результат и на Android, и на iOS.
-
Измените логику проверки пароля пользователя: «password» не должен считаться допустимым вариантом. Для этого обновите функцию
checkPassword()классаLoginDataValidator(чтобы быстро найти его, дважды нажмите Shift, вставьте имя класса и перейдите на вкладку Classes):package com.jetbrains.simplelogin.shared.data class LoginDataValidator { //... fun checkPassword(password: String): Result { return when { password.length < 5 -> Result.Error("Password must be >5 characters") password.lowercase() == "password" -> Result.Error("Password shouldn't be \"password\"") else -> Result.Success } } //... } -
Запустите приложения для iOS и Android из Android Studio, чтобы увидеть изменения (сообщение об ошибке на iOS появится после нажатия на красный предупреждающий треугольник):

Вы можете посмотреть итоговый код этого руководства.
Чем ещё можно поделиться?
Вы поделились бизнес-логикой приложения, но можете также решить поделиться другими его слоями. Например, код класса ViewModel почти одинаков для приложения для Android и приложения для iOS, и вы можете использовать его совместно, если в мобильных приложениях должен быть одинаковый слой представления.
Что дальше?
После того как вы перевели приложение для Android на мультиплатформенную архитектуру, можно продолжить и:
С помощью Compose Multiplatform можно создать единый интерфейс для всех платформ:
Также ознакомьтесь с материалами сообщества:
© 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-integrate-in-existing-app.html