Превратите ваше Android-приложение в приложение для iOS – учебник
Узнайте, как превратить ваше существующее Android-приложение в кроссплатформенное, чтобы оно работало как на Android, так и на iOS. Вы сможете написать код и протестировать его для Android и iOS только один раз, в одном месте.
В этом руководстве используется пример Android-приложения с одним экраном для ввода имени пользователя и пароля. Данные учетных данных проверяются и сохраняются в локальной базе данных.
Если вы не знакомы с Kotlin Multiplatform Mobile, сначала узнайте, как создать и настроить кроссплатформенное мобильное приложение с нуля.
Подготовка среды разработки
-
Установите Android Studio 4.2 или Android Studio 2020.3.1 Canary 8 или выше и другие инструменты для кроссплатформенной разработки мобильных приложений на macOS.
-
В Android Studio создайте новый проект из системы контроля версий:
https://github.com/Kotlin/kmm-integration-sample
Ветвь
masterсодержит начальное состояние проекта — простое Android-приложение. Чтобы увидеть конечное состояние с приложением iOS и общим модулем, переключитесь на ветвьfinal. -
Переключитесь на представление Проект.
Сделайте свой код кроссплатформенным
Чтобы ваше приложение работало на iOS, вы сначала сделаете свой код кроссплатформенным, а затем повторно используете свой кроссплатформенный код в новом приложении для iOS.
Чтобы сделать свой код кроссплатформенным:
Определите, какой код следует сделать кроссплатформенным
Определите, какой код вашего Android-приложения лучше всего использовать совместно для iOS, а какой оставить нативными. Простое правило: совместно используйте то, что вы хотите максимально переиспользовать. Бизнес-логика часто одинакова для Android и iOS, поэтому она отлично подходит для повторного использования.
В вашем образце Android-приложения бизнес-логика хранится в пакете com.jetbrains.simplelogin.androidapp.data. Ваше будущее приложение для iOS будет использовать ту же логику, поэтому вам также следует сделать ее кроссплатформенной.
Создайте общий модуль для кроссплатформенного кода
Кроссплатформенный код, используемый как для iOS, так и для Android, хранится в общем модуле. Плагин Kotlin Multiplatform Mobile предоставляет специальный мастер для создания таких модулей.
В своем проекте Android создайте общий модуль Kotlin Multiplatform для своего кроссплатформенного кода. Позже вы подключите его к существующему Android-приложению и вашему будущему iOS-приложению.
В Android Studio нажмите Файл | Новый | Новый модуль.
-
В списке шаблонов выберите Общий модуль Kotlin Multiplatform, введите имя модуля
shared, и выберите Регулярный фреймворк в списке вариантов распространения фреймворков iOS.
Это необходимо для подключения общего модуля к приложению iOS. Нажмите Готово.
Мастер создаст общий модуль Kotlin Multiplatform, обновит файлы конфигурации и создаст файлы с классами, демонстрирующими преимущества Kotlin Multiplatform. Вы можете узнать больше о структуре проекта.
Добавьте зависимость от общего модуля в ваше Android-приложение
Чтобы использовать кроссплатформенный код в вашем Android-приложении, подключите общий модуль к нему, перенесите код бизнес-логики туда и сделайте этот код кроссплатформенным.
-
В файле
build.gradle.ktsобщего модуля убедитесь, чтоcompileSdkиminSdkсовпадают с теми, что в файлеbuild.gradleвашего Android-приложения в модулеapp.Если они отличаются, обновите их в
build.gradle.ktsобщего модуля. В противном случае возникнет ошибка компиляции. -
Добавьте зависимость от общего модуля в
build.gradleвашего Android-приложения.dependencies { implementation project(':shared') } -
Синхронизируйте файлы Gradle, нажав Синхронизировать сейчас в сообщении об ошибке.
В каталоге
app/src/main/java/, откройте классLoginActivityв пакетеcom.jetbrains.simplelogin.androidapp.ui.login.-
Чтобы убедиться, что общий модуль успешно подключен к вашему приложению, выведите результат функции
greeting()в журнал, обновив методonCreate().override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("Login Activity", "Hello from shared module: " + (Greeting().greeting())) } Следуйте предложениям Android Studio для импорта отсутствующих классов.
-
Отладьте
app. На вкладке Logcat найдитеHelloв журнале, и вы найдете приветствие из общего модуля.
Сделайте бизнес-логику кроссплатформенной
Теперь вы можете извлечь код бизнес-логики в общий модуль Kotlin Multiplatform и сделать его независимым от платформы. Это необходимо для повторного использования кода как для Android, так и для iOS.
-
Перенесите код бизнес-логики
com.jetbrains.simplelogin.androidapp.dataиз каталогаappв пакетcom.jetbrains.simplelogin.sharedв каталогеshared/src/commonMain. Вы можете перетащить пакет или переструктурировать его, перенеся все из одного каталога в другой. -
Когда Android Studio спросит, что вы хотите сделать, выберите перемещение пакета, а затем одобрите переструктурирование.
-
Проигнорируйте все предупреждения о платформенно-зависимом коде и нажмите Продолжить.

Удалите код, специфичный для Android, заменив его кроссплатформенным кодом Kotlin или подключив к API, специфичным для Android, с помощью
expectиactualобъявлений. Подробности см. в следующих разделах:
Замените код, специфичный для Android, кроссплатформенным кодом
Чтобы ваш код хорошо работал как на Android, так и на iOS, замените все зависимости JVM на зависимости Kotlin в перенесенном каталоге data по возможности.
-
В классе
LoginDataSourceзаменитеIOExceptionв функцииlogin()наRuntimeException.IOExceptionнедоступно в Kotlin.// Before return Result.Error(IOException("Error logging in", e))// After return Result.Error(RuntimeException("Error logging in", e)) -
В классе
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() }
Подключение к платформа-специфичным API из кроссплатформенного кода
В классе 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из общего кода: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.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.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()
Для Android и iOS Kotlin будет использовать разные платформенно-специфичные реализации.
Запуск кроссплатформенного приложения на Android
Запустите ваше кроссплатформенное приложение для Android, чтобы убедиться в его работоспособности.

Настройка работы кроссплатформенного приложения на iOS
После того, как вы сделали своё приложение кроссплатформенным для Android, вы можете создать приложение для iOS и повторно использовать общий бизнес-логик в нём.
Создание проекта iOS в Xcode
В Xcode, нажмите Файл | Новый | Проект.
-
Выберите шаблон для приложения iOS и нажмите Далее.

-
В качестве имени продукта укажите simpleLoginIOS и нажмите Далее.
В качестве места расположения проекта выберите каталог, содержащий ваше кроссплатформенное приложение, например,
kmm-integration-sample.
В Android Studio вы получите следующую структуру:
Для согласованности с другими верхнеуровневыми каталогами вашего кроссплатформенного проекта вы можете переименовать каталог simpleLoginIOS в iosApp.
Подключение фреймворка к проекту iOS
После получения фреймворка, вы можете подключить его к вашему проекту iOS вручную.
Вручную подключите ваш фреймворк к проекту iOS:
В Xcode откройте настройки проекта, дважды щелкнув имя проекта.
-
На вкладке Этапы сборки настроек проекта, нажмите + и добавьте Новый этап выполнения скрипта.

-
Добавьте следующий скрипт:
cd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode
-
Переместите этап Выполнение скрипта перед этапом Компиляция исходных кодов.
-
На вкладке Настройки сборки переключитесь на Все настройки сборки и укажите Путь поиска фреймворка в разделе Пути поиска:
$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)

-
На вкладке Настройки сборки укажите Другие флаги компоновщика в разделе Компоновка:
$(inherited) -framework shared

Соберите проект в Xcode. Если всё настроено правильно, проект будет успешно собран.
Использование общего модуля из Swift
-
В Xcode откройте файл
ContentView.swiftи импортируйте модульshared.import shared
-
Для проверки правильности подключения, используйте функцию
greeting()из общего модуля вашего кроссплатформенного приложения:import SwiftUI import shared struct ContentView: View { var body: some View { Text(Greeting().greeting()) .padding() } } -
В
ContentView.swift, напишите код для использования данных из общего модуля и отображения пользовательского интерфейса приложения:import SwiftUI import shared 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: "Логин", secured: false, text: $username, errorMessage: viewModel.formState.usernameError, onChange: { viewModel.loginDataChanged(username: username, password: password) }) ValidatedTextField(titleKey: "Пароль", secured: true, text: $password, errorMessage: viewModel.formState.passwordError, onChange: { viewModel.loginDataChanged(username: username, password: password) }) Button("Войти") { 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("Ошибка"), message: Text(error), dismissButton: .default(Text("Понятно!"))) } } } 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("Успешный вход. Добро пожаловать, \(result.data.displayName)") } else { print("Ошибка при входе") } } 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, импортируйте модульsharedи укажите аргументы для функцииContentView().import SwiftUI import shared @main struct SimpleLoginIOSApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: .init(loginRepository: LoginRepository(dataSource: LoginDataSource()), loginValidator: LoginDataValidator())) } } }
Наслаждайтесь результатами – обновляйте логику только один раз
Теперь ваше приложение кроссплатформенное. Вы можете обновить бизнес-логику в одном месте и увидеть результаты как на Android, так и на iOS.
-
В Android Studio измените логику проверки пароля пользователя в функции
checkPassword()классаLoginDataValidator.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 } } //... } -
Обновите
gradle.propertiesдля подключения вашего приложения iOS к Android Studio, чтобы запускать его на эмуляторе или реальном устройстве прямо там:xcodeproj=iosApp/SimpleLoginIOS.xcodeproj
-
Синхронизируйте файлы Gradle, нажав Синхронизировать сейчас в сообщении об ошибке.
Вы увидите новую конфигурацию запуска simpleLoginIOS для запуска вашего приложения iOS прямо из Android Studio.


Вы можете ознакомиться с конечным кодом этого руководства.
Что еще можно поделиться?
Вы поделились бизнес-логикой своего приложения, но также можете поделиться другими слоями вашего приложения. Например, код класса ViewModel почти одинаков для Android и приложений iOS, и вы можете поделиться им, если ваши мобильные приложения должны иметь одинаковый слой представления.
Что дальше?
После того, как вы сделали свое Android-приложение кроссплатформенным, вы можете перейти к:
Вы также можете ознакомиться с ресурсами сообщества:
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform-mobile-integrate-in-existing-app.html