Создание 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 предоставляет специального мастера для создания таких модулей.
В вашем проекте 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.ktsвашего Android-приложения в модулеapp.Если они отличаются, обновите их в
build.gradle.ktsобщего модуля. В противном случае возникнет ошибка компиляции. -
Добавьте зависимость от общего модуля в
build.gradle.ktsвашего Android-приложения.dependencies { implementation (project(":shared")) } -
Синхронизируйте файлы Gradle, нажав Синхронизировать сейчас в уведомлении.
В каталоге
app/src/main/java/откройте классLoginActivityв пакетеcom.jetbrains.simplelogin.androidapp.ui.login.-
Чтобы убедиться, что общий модуль успешно подключен к вашему приложению, выведите результат функции
greet()в журнал, обновив методonCreate().override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("Login Activity", "Hello from shared module: " + (Greeting().greet())) } Следуйте рекомендациям 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:import com.jetbrains.simplelogin.shared.randomUUID
Для 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
-
Для проверки корректной связи, используйте функцию
greet()из общего модуля вашего кроссплатформенного приложения:import SwiftUI import shared struct ContentView: View { var body: some View { Text(Greeting().greet()) .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 } } //... } -
Запустите приложения iOS и Android из 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