Spec-Zone.ru › Kotlin 2

Как сделать Android-приложение совместимым с iOS — руководство

В этом руководстве показано, как сделать существующее Android-приложение кроссплатформенным, чтобы оно работало и на Android, и на iOS. Вы сможете одновременно писать код для Android и iOS в одном месте.

В этом руководстве используется пример Android-приложения с одним экраном для ввода имени пользователя и пароля. Учетные данные проверяются и сохраняются в базе данных в памяти.

Чтобы приложение работало и на iOS, и на Android, сначала сделайте код кроссплатформенным, переместив его часть в общий модуль. Затем используйте кроссплатформенный код в Android-приложении, а после этого — тот же код в новом iOS-приложении.

Если вы не знакомы с Kotlin Multiplatform, сначала узнайте, как создать кроссплатформенное приложение с нуля.

Подготовьте среду разработки

  1. В кратком руководстве выполните инструкции по настройке среды для разработки на Kotlin Multiplatform.

    Для выполнения некоторых шагов этого руководства, например запуска iOS-приложения, понадобится Mac с macOS. Это требование Apple.

  2. В Android Studio создайте новый проект из системы контроля версий:

    https://github.com/Kotlin/kmp-integration-sample
    

    В ветке master находится исходное состояние проекта — простое Android-приложение. Чтобы увидеть итоговое состояние с iOS-приложением и общим модулем, переключитесь на ветку final.

  3. Переключитесь в представление Проект:

    Project view

Сделайте код кроссплатформенным

Чтобы сделать код кроссплатформенным, выполните следующие действия:

  1. Решите, какой код сделать кроссплатформенным

  2. Создайте общий модуль для кроссплатформенного кода

  3. Проверьте совместное использование кода

  4. Добавьте зависимость от общего модуля в Android-приложение

  5. Сделайте бизнес-логику кроссплатформенной

  6. Запустите кроссплатформенное приложение на Android

Решите, какой код сделать кроссплатформенным

Решите, какой код Android-приложения лучше использовать совместно с iOS, а какой оставить нативным. Простое правило: делитесь тем, что хотите использовать повторно как можно чаще. Бизнес-логика часто одинакова для Android и iOS, поэтому она отлично подходит для повторного использования.

В примере Android-приложения бизнес-логика хранится в пакете com.jetbrains.simplelogin.androidapp.data. Будущее iOS-приложение будет использовать ту же логику, поэтому ее тоже следует сделать кроссплатформенной.

Business logic to share

Создайте общий модуль для кроссплатформенного кода

Кроссплатформенный код, используемый и на iOS, и на Android, будет храниться в общем модуле. В Android Studio и IntelliJ IDEA есть мастер создания общих модулей для Kotlin Multiplatform.

Создайте общий модуль, который будет связан с существующим Android-приложением и будущим iOS-приложением:

  1. В Android Studio выберите в главном меню Файл | Создать | Создать модуль.

  2. В списке шаблонов выберите Общий модуль Kotlin Multiplatform. Оставьте имя модуля shared и укажите имя пакета:

    com.jetbrains.simplelogin.shared
    
  3. Нажмите Готово. Мастер создаст общий модуль, соответствующим образом изменит скрипт сборки и запустит синхронизацию Gradle.

  4. Дождитесь завершения синхронизации. В каталоге shared появится следующая структура файлов:

    Final file structure inside the shared directory

    Чтобы лучше понять структуру получившегося проекта, ознакомьтесь с основами структуры проекта Kotlin Multiplatform.

  5. Замените блок 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:

  1. Создайте класс Greeting со следующим кодом:

    package com.jetbrains.simplelogin.shared
    
    class Greeting {
        private val platform = getPlatform()
    
        fun greet(): String {
            return "Hello, ${platform.name}!"
        }
    }
    
  2. Замените код в существующих файлах следующим:

    • В 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-приложении, подключите к нему общий модуль, переместите туда код бизнес-логики и сделайте этот код кроссплатформенным.

  1. Добавьте зависимость от общего модуля в файл app/build.gradle.kts:

    dependencies {
        // ...
        implementation(project(":shared"))
    }
    
  2. Синхронизируйте файлы Gradle, следуя предложению IDE или выбрав пункт меню Файл | Синхронизировать проект с файлами Gradle.

  3. В каталоге app/src/main/java/ откройте файл LoginActivity.kt в пакете com.jetbrains.simplelogin.androidapp.ui.login.

  4. Чтобы убедиться, что общий модуль успешно подключен к приложению, запишите результат функции greet() в журнал, добавив вызов Log.i() в метод onCreate():

    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
    
        Log.i("Login Activity", "Hello from shared module: " + (Greeting().greet()))
    
        // ...
    }
    
  5. Следуйте рекомендациям IDE, чтобы импортировать недостающие классы.

  6. На панели инструментов нажмите значок отладки рядом с раскрывающимся списком конфигураций запуска:

    App from list to debug
  7. В окне инструментов Logcat найдите в журнале строку «Hello» — вы увидите приветствие из общего модуля:

    Greeting from the shared module

Сделайте бизнес-логику кроссплатформенной

Теперь можно извлечь код бизнес-логики в набор исходных файлов commonMain общего модуля Kotlin Multiplatform. Это позволит использовать код и на Android, и на iOS.

  1. Переместите код бизнес-логики com.jetbrains.simplelogin.androidapp.data из каталога app в пакет com.jetbrains.simplelogin.shared в каталоге shared/src/commonMain.

    Drag and drop the package with the business logic code
  2. Когда Android Studio спросит, что нужно сделать, выберите перемещение пакета и подтвердите рефакторинг.

    Refactor the business logic package
  3. Игнорируйте все предупреждения о зависимом от платформы коде и нажмите Все равно выполнить рефакторинг.

    Warnings about platform-dependent code
  4. Удалите специфичный для Android код, заменив его кроссплатформенным кодом Kotlin или подключившись к специфичным для Android API с помощью объявлений expected и actual. Подробности см. в следующих разделах:

    Замените специфичный для Android код кроссплатформенным кодом

    Чтобы код корректно работал и на Android, и на iOS, по возможности замените все зависимости JVM на зависимости Kotlin в перемещенном каталоге data.

    1. В классе 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()
      }
      
    2. Удалите директиву импорта класса Patterns:

      import android.util.Patterns
      
    3. В классе 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))
      ```
      
    4. Также удалите директиву импорта для 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.

    1. Замените вызов java.util.UUID.randomUUID() в функции login() вызовом randomUUID(), который вы реализуете для каждой платформы:

      val fakeUser = LoggedInUser(randomUUID(), "Jane Doe")
      
    2. Создайте файл Utils.kt в пакете com.jetbrains.simplelogin.shared каталога shared/src/commonMain и добавьте объявление expect:

      package com.jetbrains.simplelogin.shared
      
      expect fun randomUUID(): String
      
    3. Создайте файл 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()
      
    4. Создайте файл 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()
      
    5. Импортируйте функцию randomUUID в файл LoginDataSource.kt каталога shared/src/commonMain:

      import com.jetbrains.simplelogin.shared.randomUUID
      

Теперь Kotlin будет использовать специфичные для платформы реализации UUID для Android и iOS.

Запустите кроссплатформенное приложение на Android

Запустите конфигурацию запуска app, чтобы убедиться, что Android-приложение работает как прежде.

Android login application

Сделайте кроссплатформенное приложение совместимым с iOS

После того как вы сделаете Android-приложение кроссплатформенным, можно создать iOS-приложение и повторно использовать в нем общую бизнес-логику.

  1. Создайте iOS-проект в Xcode

  2. Настройте iOS-проект для использования фреймворка KMP

  3. Настройте конфигурацию запуска iOS в Android Studio

  4. Используйте общий модуль в iOS-проекте

Создайте iOS-проект в Xcode

  1. В Xcode нажмите Файл | Создать | Проект.

  2. В диалоговом окне перейдите на вкладку iOS:

    iOS project template
  3. Выберите шаблон Приложение, затем нажмите Далее.

  4. Укажите в качестве имени продукта «simpleLoginIOS» и нажмите Далее.

    iOS project settings
  5. В качестве расположения проекта выберите каталог, в котором хранится кроссплатформенное приложение, например kmp-integration-sample.

    В Android Studio вы увидите следующую структуру:

    iOS project in Android Studio
  6. Для единообразия с другими каталогами верхнего уровня кроссплатформенного проекта закройте Xcode и переименуйте каталог simpleLoginIOS в iosApp.

    Если переименовать папку при открытом Xcode, появится предупреждение и проект может быть поврежден.

    Renamed iOS project directory in Android Studio

Настройте iOS-проект для использования фреймворка KMP

Можно настроить интеграцию между iOS-приложением и фреймворком, собранным Kotlin Multiplatform.

Альтернативные способы интеграции (SwiftPM и CocoaPods) описаны в обзоре способов интеграции с iOS.

  1. В Android Studio щелкните правой кнопкой мыши каталог iosApp/simpleLoginIOS.xcodeproj и выберите Открыть в | Открыть в связанном приложении, чтобы открыть iOS-проект в Xcode.

  2. В Xcode нажмите имя проекта в навигаторе Проект, чтобы открыть настройки iOS-проекта.

  3. В разделе Цели слева выберите simpleLoginIOS, затем откройте вкладку Этапы сборки.

  4. Нажмите значок + и выберите Новый этап Run Script.

    Add a run script phase
  5. Вставьте следующий скрипт в поле скрипта запуска:

    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
    
  6. Отключите параметр На основе анализа зависимостей. Это гарантирует, что Xcode будет запускать скрипт при каждой сборке и не будет каждый раз предупреждать об отсутствующих зависимостях выходных данных.

    Add the script
  7. Переместите этап Run Script выше — перед этапом Компиляция исходных файлов:

    Move the Run Script phase
  8. На вкладке Параметры сборки отключите параметр Песочница пользовательских скриптов в разделе Параметры сборки:

    User Script Sandboxing

    Если у вас используется нестандартная конфигурация сборки, отличная от конфигураций по умолчанию Debug или Release, на вкладке Параметры сборки добавьте параметр KOTLIN_FRAMEWORK_BUILD_TYPE в раздел Пользовательские и задайте для него значение Debug или Release.

  9. На вкладке Информация добавьте пользовательское свойство CADisableMinimumFrameDurationOnPhone и задайте для него значение YES, чтобы включить высокую частоту обновления экрана в iOS.

  10. На вкладке Подписание и возможности выберите команду разработки или создайте ее, если еще не сделали этого. Это позволит подписать фреймворк shared, созданный модулем KMP.

    Здесь также убедитесь, что для параметра Идентификатор пакета задано уникальное значение, иначе сборка в Xcode может завершиться с ошибкой.

  11. Соберите проект в Xcode (в главном меню выберите Продукт | Собрать). Если все настроено правильно, сборка проекта завершится успешно (предупреждение «этап сборки будет выполняться при каждой сборке» можно безопасно проигнорировать).

    Сборка может завершиться с ошибкой, если вы собирали проект до отключения параметра Песочница пользовательских скриптов: процесс демона Gradle может выполняться в песочнице, и его необходимо перезапустить. Перед повторной сборкой проекта остановите его, выполнив эту команду в каталоге проекта (в нашем примере — kmp-integration-sample):

    ./gradlew --stop
    

Настройте конфигурацию запуска iOS в Android Studio

Убедившись, что Xcode настроен правильно, вернитесь в Android Studio:

  1. В главном меню выберите Файл | Синхронизировать проект с файлами Gradle. Android Studio автоматически создаст конфигурацию запуска simpleLoginIOS.

    Android Studio автоматически создаст конфигурацию запуска simpleLoginIOS и отметит каталог iosApp как связанный проект Xcode.

  2. В списке конфигураций запуска выберите simpleLoginIOS. Выберите эмулятор iOS, затем нажмите Запустить, чтобы проверить работу конфигурации запуска iOS.

    The iOS run configuration in the list of run configurations

Используйте общий модуль в iOS-проекте

Файл shared/build.gradle.kts задает свойство binaries.framework.baseName для каждой цели iOS со значением sharedKit. Это имя фреймворка, который Kotlin Multiplatform собирает для использования в iOS-приложении.

Чтобы проверить интеграцию, добавьте вызов общего кода в код Swift:

  1. В Android Studio откройте файл iosApp/simpleloginIOS/ContentView.swift и импортируйте фреймворк:

    import sharedKit
    
  2. Чтобы убедиться, что подключение выполнено правильно, обновите код структуры ContentView, чтобы использовать функцию greet() из модуля shared:

    struct ContentView: View {
        var body: some View {
            Text(Greeting().greet())
            .padding()
        }
    }
    
  3. Запустите приложение с помощью конфигурации запуска iOS в Android Studio, чтобы увидеть результат:

    Greeting from the shared module
  4. Снова обновите код в файле 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) } } }
  5. В файле 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()))
            }
        }
    }
    
  6. Еще раз запустите конфигурацию запуска iOS, чтобы убедиться, что в iOS-приложении отображается форма входа.

  7. Введите «Jane» в качестве имени пользователя и «password» в качестве пароля.

  8. Поскольку вы настроили интеграцию ранее, iOS-приложение проверяет введенные данные с помощью общего кода:

    Simple login application

Наслаждайтесь результатами — обновляйте логику только один раз

Теперь ваше приложение работает на нескольких платформах. Вы можете обновить бизнес-логику в модуле shared и увидеть результат и на Android, и на iOS.

  1. Измените логику проверки пароля пользователя: «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
            }
        }
    //...
    }
    
  2. Запустите приложения для iOS и Android из Android Studio, чтобы увидеть изменения (сообщение об ошибке на iOS появится после нажатия на красный предупреждающий треугольник):

    Android and iOS applications password error

Вы можете посмотреть итоговый код этого руководства.

Чем ещё можно поделиться?

Вы поделились бизнес-логикой приложения, но можете также решить поделиться другими его слоями. Например, код класса ViewModel почти одинаков для приложения для Android и приложения для iOS, и вы можете использовать его совместно, если в мобильных приложениях должен быть одинаковый слой представления.

Что дальше?

После того как вы перевели приложение для Android на мультиплатформенную архитектуру, можно продолжить и:

  • добавить зависимости от мультиплатформенных библиотек

  • добавить зависимости Android

  • добавить зависимости iOS

С помощью Compose Multiplatform можно создать единый интерфейс для всех платформ:

  • узнайте о Compose Multiplatform и Jetpack Compose

  • изучите доступные ресурсы для Compose Multiplatform

  • создайте приложение с общей логикой и интерфейсом

Также ознакомьтесь с материалами сообщества:

  • Видео: как перенести проект Android на Kotlin Multiplatform

  • Видео: 3 способа подготовить код Kotlin JVM к использованию в Kotlin Multiplatform

21 июля 2026 г.
Завершение проектаПеренос приложения Jetpack Compose на Kotlin 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API