Превратите ваше приложение Android в приложение для iOS – учебник
Узнайте, как сделать ваше существующее приложение Android кроссплатформенным, чтобы оно работало как на Android, так и на iOS. Вы сможете написать код и протестировать его для Android и iOS только один раз, в одном месте.
В этом учебнике используется пример приложения Android с единственным экраном для ввода имени пользователя и пароля. Данные учетных данных проверяются и сохраняются в базе данных в оперативной памяти.
Подготовка среды разработки
-
Установите все необходимые инструменты и обновите их до последних версий.
-
В Android Studio создайте новый проект из системы контроля версий:
https://github.com/Kotlin/kmm-integration-sample
-
Переключитесь на представление Проект.

Сделайте код кроссплатформенным
Чтобы ваше приложение работало на 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()
-
Осталось импортировать
randomUUIDв файлLoginDataSource.ktкаталогаshared/src/commonMain.Для Android и iOS Kotlin будет использовать свои платформенно-специфичные реализации.
import com.jetbrains.simplelogin.shared.randomUUID
Запустите ваше кроссплатформенное приложение на 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–2023 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