Любая технология, направленная на предоставление решения для многоплатформенной разработки, подходит к проблеме обработки различий между платформами с новой точки зрения.
Когда вы пишете программу на языке высокого уровня, таком как C или Java, вы должны скомпилировать ее для работы на такой платформе, как Windows или Linux. Было бы замечательно, если бы компиляторы могли взять один и тот же код и создать форматы, понятные для разных платформ. Однако это легче сказать, чем сделать.
Kotlin Multiplatform использует эту концепцию и обещает запускать практически один и тот же высокоуровневый код на нескольких платформах — например, JVM, JS или нативных платформах, таких как iOS, напрямую.
В отличие от Java, KMP не зависит от виртуальной машины, работающей на целевой платформе. Он предоставляет компиляторы и библиотеки для конкретных платформ, такие как Kotlin/JVM, Kotlin/JS и Kotlin/Native.
В этой главе вы узнаете, как структурировать свой код в соответствии с предложенным KMP подходом к обработке лакомых кусочков, специфичных для платформы.
Повторное использование кода между платформами
Kotlin Multiplatform не компилирует весь общий модуль для всех платформ в целом. Вместо этого определенный объем кода является общим для всех платформ, а определенный объем общего кода специфичен для каждой платформы. Для этого он использует механизм под названием expect/actual .
В главе 1 вы познакомились с этими двумя новыми ключевыми словами. Теперь вы углубитесь в эту концепцию.
Думайте об expect
этом как о прославленном интерфейсе в Kotlin или протоколе в Swift. Вы определяете классы, свойства и функции expect
, говоря, что общий общий код ожидает , что что- то будет доступно на всех платформах. Кроме того, вы используете actual
для обеспечения фактической реализации на каждой платформе.
Подобно интерфейсу или протоколу, объекты с тегом expect
не включают код реализации. Вот где actual
приходит.
После определения ожидаемых объектов их можно легко использовать в общем коде. KMP использует соответствующий компилятор для компиляции кода, который вы написали для каждой платформы. Например, он использует Kotlin/JVM для Android и Kotlin/Native для iOS или macOS. Позже в процессе компиляции каждый из них будет объединен с скомпилированной версией общего кода для соответствующих платформ.
Вы можете спросить, зачем вам это нужно в первую очередь. Иногда вам нужно вызывать методы, специфичные для каждой платформы. Например, вы можете использовать Core ML на платформах Apple или ML Kit на Android для машинного обучения. Вы можете определить определенные ожидаемые классы, методы и свойства в общем коде и обеспечить фактическую реализацию по-разному для каждой платформы.
Механизм ожидаемого/фактического позволяет вам обращаться к собственным библиотекам каждой платформы с помощью Kotlin. Как это круто!
Поздоровайся с организацией
После того, как вы создадите отличное приложение, чтобы найти подходящее время для организации ваших международных встреч, вам понадобится способ делать TODO и напоминания для этих встреч. Организация поможет вам сделать именно это.
Как и во многих приложениях, которые вы используете каждый день, в Organize есть страница, на которой отображается информация об устройстве, на котором работает приложение. Если вы когда-либо сталкивались с ошибкой в своих приложениях, вы знаете, насколько ценной может быть эта информация при отладке.
Откройте стартовый проект для этого раздела. В основном это недавно созданный проект с использованием плагина KMM для Android Studio с уже установленными дополнительными платформами и зависимостями. Основное отличие от проекта, который вы создали в разделе 1, заключается в том, что на этот раз вы собираетесь использовать обычную платформу вместо Cocoapods для распространения платформы iOS. В конце концов, узнавать что-то новое всегда приятно.
Задолго до того, как Apple представила Swift Package Manager , разработчики iOS использовали разные подходы к управлению зависимостями. В то время как Cocoapods был — или до сих пор — де-факто способом управления зависимостями, некоторые люди склонны использовать другие подходы по многим причинам.
Некоторые преимущества использования Cocoapods:
- Это легко настроить.
- Он обрабатывает весь процесс создания зависимостей и связывания их с вашим проектом.
- У него большое сообщество, которое может помочь, если что-то пойдет не так.
- Практически вы можете найти любую зависимость в их централизованном репозитории. У него также есть веб-сайт для более удобного поиска и поиска.
Некоторые недостатки использования Cocoapods:
- Это требует установки Ruby на вашем компьютере для разработки или на любом компьютере, который вы используете для непрерывной интеграции. Вам также необходимо определить зависимости в отдельном файле, используя язык Ruby, который многим не нравится.
- По сути, он берет на себя управление вашим проектом Xcode, добавляя рабочую область и пару сценариев сборки, что может вызвать проблемы по мере увеличения вашего проекта.
- Обычно это приводит к увеличению времени сборки, поскольку каждый раз, когда вы создаете свой проект, Xcode создает все ваши зависимости.
Если вы когда-нибудь решите создать новый проект самостоятельно, вы можете изменить параметр распространения платформы iOS на следующей странице.

Рис. 6.1. Выбор опции «Обычный фреймворк» для дистрибутива фреймворка iOS
Обновление класса платформы
Как объяснялось ранее, вы собираетесь создать страницу для своих приложений, на которой вы будете отображать информацию об устройстве, на котором запущено приложение.
Если на то пошло, большая часть вашей работы в этой главе относится к Platform
классу.
Структура папок
В Android Studio выберите представление проекта в Project Navigator. Внутри общего модуля проверьте структуру каталогов.

Рис. 6.2 — Структура папок в Android Studio
Внутри проекта уже есть файл Platform.kt ; точнее, их четыре — по одной на каждую поддерживаемую вами платформу плюс одна. Чтобы механизм ожидаемого/фактического работал, вам необходимо определить сущности expect
и actual
в одном и том же пакете на каждой платформе.
На изображении выше expect
класс Platform находится внутри пакета com.raywenderlich.organize в каталоге commonMain . Реализации actual
для iOS, Android и рабочего стола находятся внутри одного пакета в iosMain , androidMain и desktopMain соответственно.
Создание класса Platform для модуля Common
Откройте Platform.kt в папке commonMain . Замените expect
определение класса следующим:
expect class Platform() { val osName: String val osVersion: String val deviceModel: String val cpuType: String val screen: ScreenInfo? fun logSystemInfo() } expect class ScreenInfo() { val width: Int val height: Int val density: Int }
Написав это, вы даете KMP обещание предоставить эту информацию. Как видите, здесь нет никакой реализации. Вы определяете, что хотите, точно так же, как interface
или protocol
.
Примечание . Вы использовали сокращенную нотацию Kotlin для определения конструктора с помощью
Platform()
. Это означает, что теперь KMP ожидает, что вы предоставите реализацию конструктора наряду со свойствами и методами.
Вы можете быть удивлены тем, что не определили Platform
или ScreenInfo
как класс данных; в конце концов, эти классы кажутся идеально подходящими для класса данных, поскольку они по сути являются держателями данных.
Причина в том, что классы данных в Kotlin автоматически генерируют некоторые реализации под капотом. Следовательно, вы не можете использовать их здесь, так как ожидаемые классы не должны иметь реализации.
Вы также не можете определять вложенные классы внутри файла expect class
. Следовательно, вы определили ScreenInfo
класс вне Platform
определения. Вы также можете создать новый файл, если хотите. Выполнение этого в том же файле тоже сработает.
В поле кода нажмите на желтый ромб с буквой А внутри. Это позволяет вам перейти к фактическому файлу реализации для платформ, которые вы определили в проекте.

Рис. 6.3. Переход к фактическим файлам реализации.
Если файлы еще не были на своих местах или вы еще не реализовали фактическое определение, вы можете поместить курсор на ожидаемое имя класса и нажать Alt+Enter на клавиатуре. Android Studio поможет облегчить этот процесс. Так обстоит дело ScreenInfo
, например:

Рис. 6.4. Alt+Enter при ожидании имени класса для создания фактических классов.
Внедрение платформы на Android
Перейдите к Platform.kt внутри папки androidMain .
Вы увидите, что Android Studio уже начала уговаривать вас выполнить обещание. Ведь КМП находится в зачаточном состоянии, а вы знаете, какие бывают малыши!

Рис. 6.5 — Ошибки Android Studio в актуальном классе
Замените все определение класса этим блоком кода:
`//1
actual class Platform actual constructor() {
//2
actual val osName = «Android»
//3
actual val osVersion = «${Build.VERSION.SDK_INT}»
//4
actual val deviceModel = «${Build.MANUFACTURER} ${Build.MODEL}»
//5
actual val cpuType = Build.SUPPORTED_ABIS.firstOrNull() ?: «—«
//6
actual val screen: ScreenInfo? = ScreenInfo()
//7
actual fun logSystemInfo() {
Log.d(
«Platform»,
«($osName; $osVersion; $deviceModel; ${screen!!.width}x${screen!!.height}@${screen!!.density}x; $cpuType)»
)
}
}
// 8
actual class ScreenInfo actual constructor() {
//9
private val metrics = Resources.getSystem().displayMetrics
//10
actual val width = metrics.widthPixels
actual val height = metrics.heightPixels
actual val density = round(metrics.density).toInt()
}`
Это кажется большим количеством кода, но это довольно просто:
- Вы предоставляете фактическую реализацию для,
Platform
а также его конструктор по умолчанию. Здесь вы не можете использовать сокращенную запись, как в ожидаемом файле. Вам нужно явно поместитьactual
ключевое слово передconstructor
. - Для имени операционной системы вы указали значение
"Android"
, потому что знаете, что этот код будет скомпилирован для Android-части общего модуля. - Для версии операционной системы вы использовали версию SDK из
Build
класса в Android. Убедитесь, что Android Studio импортирует необходимый пакет:android.os.Build
. Поскольку вы находитесь внутри Android-части общего модуля, вы можете свободно использовать любой специфичный для Android API. - Для модели устройства вы использовали статические свойства
MANUFACTURER
.MODEL``Build
- К счастью, с помощью этого свойства
Build
можно указать тип процессора устройства .SUPPORTED_ABIS
Поскольку в некоторых старых версиях Android результат может быть нулевым, укажите также значение по умолчанию. - Вы инициализируете экземпляр
ScreenInfo
и сохраняете его вscreen
свойстве. Вам нужно будет явно написать тип, так как если вы этого не сделаете, типscreen
свойства не будет обнуляемым. Мало того, вы обещали, что это свойство может быть обнулено в ожидаемом файле, и всегда следует придерживаться их обещаний. Причина, по которой это тип, допускающий значение NULL, будет ясна, когда вы реализуете часть рабочего стола. - Как и в
interface
, здесь вы предоставляете реализацию функции. На данный момент вы будете использоватьLog
класс в Android для вывода всех свойств на консоль. Обязательно импортируйтеandroid.util.Log
. Вы можете безопасно развернуть свойство, допускающее значение NULLscreen
, поскольку вы инициализировали его ненулевым значением в предыдущей части. - Вы предоставляете фактическую реализацию для,
ScreenInfo
а также его конструктор по умолчанию. - Для получения свойств экрана вам понадобится
DisplayMetrics
объект. Вы можете получить это, используя этот блок кода. Как видите, у вас могут быть дополнительные свойства или функции внутри реального класса. Обязательно импортируйтеandroid.content.res.Resources
. - Вы получаете ширину, высоту и плотность экрана, используя свойство метрики, которое вы определили ранее. Импортируйте
kotlin.math.round
, чтобы иметь возможность использоватьround
функцию.
Далее вы реализуете код для iOS.
Внедрение платформы на iOS
Когда вы находитесь внутри реального файла, вы можете щелкнуть желтый ромб с буквой E в поле, чтобы перейти к ожидаемому определению. Находясь внутри Platform.kt в папке androidMain , щелкните желтый значок и вернитесь к файлу в общем каталоге. Оттуда щелкните значок A и перейдите к фактическому файлу iOS .
Основы кода, который вы собираетесь добавить, такие же, как и раньше. Однако на этот раз вы обращаетесь к специфичным для iOS платформам, таким как UIKit
, Foundation
и CoreGraphics
получаете необходимую информацию.
Замените фактическую реализацию следующим блоком кода.
actual class Platform actual constructor() { //1 actual val osName = when (UIDevice.currentDevice.userInterfaceIdiom) { UIUserInterfaceIdiomPhone -> "iOS" UIUserInterfaceIdiomPad -> "iPadOS" else -> kotlin.native.Platform.osFamily.name } //2 actual val osVersion = UIDevice.currentDevice.systemVersion //3 actual val deviceModel: String get() { memScoped { val systemInfo: utsname = alloc() uname(systemInfo.ptr) return NSString.stringWithCString(systemInfo.machine, encoding = NSUTF8StringEncoding) ?: "---" } } //4 actual val cpuType = kotlin.native.Platform.cpuArchitecture.name //5 actual val screen: ScreenInfo? = ScreenInfo() //6 actual fun logSystemInfo() { NSLog( "($osName; $osVersion; $deviceModel; ${screen!!.width}x${screen!!.height}@${screen!!.density}x; $cpuType)" ) } } actual class ScreenInfo actual constructor() { //7 actual val width = CGRectGetWidth(UIScreen.mainScreen.nativeBounds).toInt() actual val height = CGRectGetHeight(UIScreen.mainScreen.nativeBounds).toInt() actual val density = UIScreen.mainScreen.scale.toInt() }
UIKit
Есть вызываемый класс,UIDevice
из которого вы можете запросить информацию о файлеcurrentDevice
. В этом коде вы запрашиваете идиому интерфейса, чтобы различать iOS и iPadOS. УUIUserInterfaceIdiom
перечисления есть еще пара случаев. Для краткости вы использовалиPlatform
класс Kotlin/Native для поиска информации в блоке else.- Вы также можете использовать
UIDevice
для получения версии ОС. - Это, безусловно, самый неуклюжий фрагмент кода, который вы встретите в этой книге. Но не волнуйтесь: КМП обычно не такой. Он здесь, чтобы показать вам, где все может пойти не так. Objective-C по своей сути — это C. В платформах Apple есть несколько старых API, которые восходят к 1990-м годам. Поскольку они низкоуровневые и люди не используют их часто, Apple никогда не обновляла их, чтобы иметь более приятный интерфейс. Одной из них является структура C, называемая
utsname
. Поклонники Unix, ликуйте! Здесь вы вызываете функцию C через Kotlin/Native. Насколько это безумно? В этом блоке кода вы выделяете память, используяmemScoped
блок иalloc()
вызов функции. Затем вы передаете указатель на выделенное пространство памятиuname
функции, которая получает информацию об операционной системе и заполняет ее внутриsystemInfo
. Затем вы преобразуете строку C, заполненную именем машины,NSString
и возвращаете ее. Преобразование изNSString
в Kotlin String выполняется автоматически. Фу! - Чтобы получить тип процессора, вы можете еще раз покопаться в коде C или, как здесь, просто использовать
Platform
класс Kotlin/Native. - Вы инициализируете экземпляр
ScreenInfo
и сохраняете его вscreen
свойстве. Как и в случае с Android, убедитесь, что вы указали тип явно. - Реализация функции в основном такая же, как и в Android, за исключением того, что вы передаете ту же строку в
NSLog
функцию. - Вам нужно объединить свои знания
UIKit
иCoreGraphics
получить свойства экрана. Во-первых, вы используетеUIScreen
класс для получения информации обmainScreen
устройстве. Затем вы используете функцииCGRectGetWidth
и для извлечения ширины и высоты из свойств, которые являются структурой Objective-C.CGRectGetHeight``CoreGraphics``nativeBounds``CGSize
Это может выглядеть немного странно для разработчиков Kotlin и Swift. Причина в том, что вы используете номенклатуру Objective-C для сущностей. Вот как работает KMP для платформ Apple. Таким образом, функциональная совместимость создает мост между Kotlin и Objective-C.
Блок кода в Разделе 3 странный даже для разработчиков Swift. Если бы вы написали этот раздел, используя Swift, то также были бы некоторые путешествия в мир C:
let deviceModel: String = { var systemInfo = utsname() uname(&systemInfo) let str = withUnsafePointer(to: &systemInfo.machine.0) { ptr in return String(cString: ptr) } return str }()
Почему Objective-C, а не Swift, спросите вы. Хотя создатели KMP работают над функциональной совместимостью Swift, они выбрали Objective-C по нескольким причинам:
- Многие фреймворки iOS сами по себе построены на Objective-C. Даже когда вы пишете код на Swift, вы используете мост.
- Objective-C имеет более гибкую и динамичную среду выполнения, чем Swift. По-видимому, более строгие функции безопасности типов Swift затруднили бы создание совместимости KMP с технологиями Apple.
Однако использование Objective-C вместо Swift имеет некоторые проблемы. Например, вы не можете использовать некоторые недавно представленные фреймворки, Combine
поскольку они предназначены только для Swift. Кроме того, вы не можете использовать только функции или свойства расширения Swift — или даже случаи перечисления Swift — также. У вас нет другого выбора, кроме как использовать подробное наименование сущностей в Objective-C.
Реализация платформы на десктопе
Откройте Platform.kt в папке desktopMain .
Замените фактическую реализацию этим блоком:
`actual class Platform actual constructor() {
//1
actual val osName = System.getProperty(«os.name») ?: «Desktop»
//2
actual val osVersion = System.getProperty(«os.version») ?: «—«
//3
actual val deviceModel = «Desktop»
//4
actual val cpuType = System.getProperty(«os.arch») ?: «—«
//5
actual val screen: ScreenInfo? = null
//6
actual fun logSystemInfo() {
print(«($osName; $osVersion; $deviceModel; $cpuType)»)
}
}
actual class ScreenInfo actual constructor() {
//7
actual val width = 0
actual val height = 0
actual val density = 0
}`
- Настольное приложение основано на JVM. В результате вы можете использовать классы и методы JDK для получения информации об устройстве. В Java есть класс, который называется
System
. Вы можете получить имя операционной системы, используя статическийgetProperty
метод с"os.name"
параметром. Поскольку этот метод может возвращать значение null, вы предоставили результат по умолчанию. - Вы используете тот же метод, что и раньше, но на этот раз с
"os.version"
параметром. - Вы жестко кодируете значение
"Desktop"
. JVM не дает возможности узнать что-либо о производителе и модели. - Еще раз,
System
класс в помощь! Использовать"os.arch"
в качестве параметра. - Вы предоставляете null в качестве значения для
ScreenInfo
. Вы скоро узнаете, почему. - Затем вы используете
print
функцию Kotlin для вывода обычной информации на консоль. На этот раз, однако, нет никакой информации оscreen
. - Чтобы отключить компилятор, вы устанавливаете 0 в свойствах
ScreenInfo
рабочего модуля. Это не жизненно важно, поскольку вы даже не используете этот класс.
Вы сделали пару странных вещей здесь. Самое главное, вы не предоставили полезную реальную реализацию для ScreenInfo
.
В Java нет инструментария пользовательского интерфейса. Если владелец платформы хочет использовать JVM и предоставить разработчикам способ разработки пользовательских интерфейсов, он создает набор инструментов пользовательского интерфейса или использует уже доступный.
Возможно, вы слышали о Swing или Abstract Window Toolkit (AWT). Jetpack Compose for Desktop использует Swing внутри для создания настольных оконных приложений. На момент написания этой книги Jetpack Compose for Desktop не предоставлял возможности запрашивать информацию об экране за пределами компонуемых методов.
Вы также не можете использовать методы Swing или AWT непосредственно в разделе рабочего стола общего модуля. Проблема в том, что этот модуль использует Android Gradle Plugin . Поскольку у Android есть собственный набор инструментов пользовательского интерфейса, и из-за того, как Android использует Java, нет возможности использовать java
плагин вместе с Android Gradle Plugin . Поэтому здесь нельзя использовать методы AWT или Swing.
Эта проблема уже давно указана в Google Issue Tracker и Jetbrains YouTrack , и, к сожалению, для нее пока нет решения.
Теперь понятно, почему вы определили screen
свойство как необязательное, и для него нет значения на рабочем столе.
Примечание . Поскольку подключаемый модуль Android Gradle применяется ко всему модулю, среда IDE позволяет импортировать классы, связанные с Android, такие как
Log
илиDisplayMetrics
даже внутри папки рабочего стола . Однако их использование приведет к сбою вашего настольного приложения, поскольку они недоступны во время выполнения.
Делимся дополнительным кодом
Вы могли заметить, что этот logSystemInfo
метод практически использует одну и ту же строку снова и снова. Чтобы избежать такого дублирования кода, вы обратитесь к функциям расширения Kotlin.
Откройте Platform.kt в папке commonMain . Как вы знаете, вы не можете добавить реализацию к определенным здесь свойствам или функциям. Однако никто не говорил, что вы не можете использовать функции расширения Kotlin.
В конце файла добавьте это:
val Platform.deviceInfo: String get() { var result = "($osName; $osVersion; $deviceModel; " screen?.let { result += "${it.width}x${it.height}@${it.density}x; " } result += "$cpuType)" return result }
Вы создаете ту же строку, но на этот раз на основании того факта, что screen
она может быть нулевой.
Теперь вернитесь к реальным файлам и используйте это свойство внутри logSystemInfo
функций.
В Platform.kt внутри androidMain :
actual fun logSystemInfo() { Log.d("Platform", deviceInfo) }
В Platform.kt внутри iosMain :
actual fun logSystemInfo() { NSLog(deviceInfo) }
В Platform.kt внутри desktopMain :
actual fun logSystemInfo() { print(deviceInfo) }
С помощью этого метода вы можете совместно использовать код между реальными реализациями.
Обновление пользовательского интерфейса
Теперь, когда класс Platform готов, вы завершили свою работу внутри общего модуля. KMP позаботится о создании фреймворков и библиотек, которые вы сможете использовать на каждой поддерживаемой вами платформе. Теперь вы готовы создавать красивые пользовательские интерфейсы на Android, iOS и настольных компьютерах.
Андроид
Вы будете выполнять все свои задачи внутри модуля androidApp . Базовая структура приложения готова для вас. Некоторые важные файлы нуждаются в объяснении. Они также помогут вам в следующих главах. Вот как это выглядит:

Рис. 6.6 – Структура папок для Android-приложения
Внутри корневой папки есть AppScaffold.kt и AppNavHost.kt . Эти два файла настраивают экраны приложения и заставляют навигацию между ними работать должным образом. Пожалуйста, не стесняйтесь взглянуть, если вы заинтересованы.
Приложение имеет два основных экрана: RemindersView , на котором сейчас отображается простое «Hello World», и AboutView , который вы собираетесь настроить в этой главе. Иди и открой его.
Здесь ContentView — это место, где происходит все важное. Замените его реализацию следующим фрагментом:
`@Composable
private fun ContentView() {
val items = makeItems()
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(items) { row ->
RowView(title = row.first, subtitle = row.second)
}
}
}`
Здесь вы получаете элементы, которые хотите отобразить, из функции makeItems
и помещаете их в элемент LazyColumn
, который в основном представляет собой представление списка. Import Modifier
и LazyColumn
из fillMaxSize
библиотеки Compose . Добавьте следующий импорт для items
метода:
import androidx.compose.foundation.lazy.items
Вы будете реализовывать в RowView
ближайшее время.
Добавьте makeItems
ниже ContentView
:
private fun makeItems(): List<Pair<String, String>> { //1 val platform = Platform() //2 val items = mutableListOf( Pair("Operating System", "${platform.osName} ${platform.osVersion}"), Pair("Device", platform.deviceModel), Pair("CPU", platform.cpuType) ) //3 platform.screen?.let { val max = max(it.width, it.height) val min = min(it.width, it.height) items.add(Pair("Display", "${max}×${min} @${it.density}x")) } return items }
- Во-первых, вы инициализируете экземпляр
Platform
созданного ранее класса. - Затем вы создаете пары данных с заголовками и информацией из
platform
и сохраняете их в изменяемый список. - Хотя вы знаете, что
screen
свойство не равно null на Android, лучше перестраховаться, чем потом сожалеть, сталкиваясь со свойствами, допускающими значение null.
Импорт max
и min
из kotlin.math
. И для последней части этого приложения добавьте RowView
компонуемую функцию следующим образом:
@Composable private fun RowView( title: String, subtitle: String, ) { Column(modifier = Modifier.fillMaxWidth()) { Column(Modifier.padding(8.dp)) { Text( text = title, style = MaterialTheme.typography.caption, color = Color.Gray, ) Text( text = subtitle, style = MaterialTheme.typography.body1, ) } Divider() } }
Это простая вертикальная стопка текстовых элементов, которые показывают заголовок и подзаголовок. Вы можете использовать предопределенные значения типографики материала, чтобы довести дело до ума. Они аналогичны предопределенным стилям текста в функции динамического ввода iOS.
Это конец вашего путешествия по Android в этой главе. Создайте и запустите приложение и проверьте результат.
Нажмите кнопку i , чтобы просмотреть свойства устройства.

Рис. 6.7 — Первая страница Organize на Android.

Рис. 6.8. Страница «Об устройстве» в Organize на Android.
Далее вы собираетесь создать приложение для iOS.
iOS
Хотя никто не может помешать вам использовать Android Studio для редактирования файлов Swift, было бы разумнее открыть Xcode.
Внутри папки iosApp в корневом каталоге проекта откройте проект Xcode, дважды щелкнув iosApp.xcodeproj .
Файл ContentView.swift является начальной страницей приложения. Это уже там для вас. Взгляните, если хотите.
Откройте AboutView.swift и замените строку на следующую Text("Hello World")
:
AboutListView()
Затем создайте новый файл представления SwiftUI с именем AboutListView , нажав Command-N .

Рис. 6.9 — диалоговое окно создания файла Xcode
Сначала импортируйте общий модуль вверху файла:
import shared
Это фреймворк KMP, созданный для вас. Если он выдает ошибку о том, что он не найден, не волнуйтесь. Сборка проекта решит проблему.
Во-вторых, добавьте внутреннюю структуру для хранения данных, которые вы собираетесь показать:
private struct RowItem: Hashable { let title: String let subtitle: String }
Соответствие Hashable
протоколу необходимо для ForEach
структуры в SwiftUI.
В-третьих, внутри AboutListView
структуры добавьте свойство для хранения ссылки на элементы, которые вы собираетесь показывать из Platform
класса.
private let items: [RowItem] = { //1 let platform = Platform() //2 var result: [RowItem] = [ .init( title: "Operating System", subtitle: "\(platform.osName) \(platform.osVersion)" ), .init( title: "Device", subtitle: platform.deviceModel ), .init( title: "CPU", subtitle: platform.cpuType ) ] //3 if let screen = platform.screen { let width = min(screen.width, screen.height) let height = max(screen.width, screen.height) result.append( .init( title: "Display", subtitle: "\(width)×\(height) @\(screen.density)x" ) ) } //4 return result }()
- Вы создаете экземпляр
Platform
класса. - Затем вы создаете массив
RowItem
экземпляров, содержащий информацию изplatform
экземпляра. - Затем вы условно разворачиваете
screen
свойство, вычисляетеwidth
иheight
и добавляете результат в массив. - В конце вы возвращаете массив, который хотите отобразить на странице.
Наконец, замените содержимое body
свойства следующим:
var body: some View { List { ForEach(items, id: \.self) { item in VStack(alignment: .leading) { Text(item.title) .font(.footnote) .foregroundColor(.secondary) Text(item.subtitle) .font(.body) .foregroundColor(.primary) } .padding(.vertical, 4) } } }
Это очень простой список в SwiftUI. Для каждого элемента внутри items
свойства отображается вертикальная стопка текстовых элементов, состоящая из заголовка и подзаголовка. Вы также применяете множество модификаторов форматирования, таких как font
и foregroundColor
, чтобы сделать его более приятным для глаз.
Стройте и запускайте. Затем нажмите кнопку « О программе », чтобы увидеть созданную вами страницу.

Рис. 6.10 — Первая страница Организовать на iOS.

Рис. 6.11 — Страница «Об устройстве» в Organize на iOS.
Рабочий стол
В разделе 1 вы узнали, как обмениваться кодом пользовательского интерфейса между Android и настольным компьютером. Чтобы показать, что в этом нет необходимости, вы будете следовать другому подходу к Organize : вы вернетесь к проверенному временем копированию и вставке!
Настройка настольного приложения немного отличается от приложения для Android, хотя они оба используют Jetpack Compose. Одно отличие состоит в том, что вы не используете компонент навигации Jetpack в настольном приложении. Вы также открываете страницу « Об устройстве » в новом окне, чтобы больше соответствовать соглашениям о рабочем столе.
За исключением нескольких нюансов в дизайне страницы «О программе», таких как отображение каждого элемента данных в строке , а не в столбце , код остается прежним. Он есть для вас в стартовом проекте. Откройте AboutView.kt
, найдите //2
и //3
раскомментируйте код под ними.
Существует несколько способов запуска настольного приложения. Вы можете открыть меню Gradle сбоку в разделе desktopApp ▸ создать рабочий стол и нажать « Выполнить » .

Рис. 6.12 — Меню Gradle, запуск настольного приложения
Приложение работает и очень похоже на приложение для Android.

Рис. 6.13 — Первая страница Организовать на рабочем столе.

Рис. 6.14. Страница «Об устройстве» приложения «Организовать на рабочем столе».
Испытание
Вот задача для вас, чтобы практиковать то, что вы узнали. Решение всегда находится в материалах этой главы, так что не беспокойтесь и не торопитесь.
Задача: создать общий регистратор
Вы можете вызывать другие ожидаемые функции внутри ожидаемых/фактических реализаций. Как вы помните, внутри класса была logSystemInfo
функция , где она использовалась и на соответствующей платформе.Platform``NSLog``Log
Рефакторинг этих вызовов в новый класс с именем Logger
. В качестве бонуса вы можете добавить уровни логирования в свою реализацию.
Ключевые моменты
- Вы можете использовать механизм ожидаемого/фактического вызова собственных библиотек каждой платформы с помощью Kotlin.
- Ожидайте, что сущности ведут себя так же, как
interface
илиprotocol
. - На платформах Apple Kotlin использует Objective-C для взаимодействия.
- Вы можете добавить общую реализацию для ожидаемых объектов с помощью функций расширения Kotlin.