KA0717 — Совместное использование пользовательского интерфейса для создания между Android и Desktop

  • Запись изменена:08.12.2022
  • Post category:Публикации
  • Reading time:44 минут чтения

Из этой книги вы узнали, как совместно использовать свою бизнес-логику в приложениях для Android, iOS и настольных ПК. Что, если бы вы могли пойти еще дальше и поделиться своим пользовательским интерфейсом Compose?

Правильно — наряду с Kotlin Multiplatform у вас теперь есть Compose Multiplatform , который позволяет вам делиться своим пользовательским интерфейсом Compose с приложениями для Android и настольных компьютеров.

Примечание . В этом приложении используется Learn — проект, созданный вами в главах с 11 по 14.

Обновление структуры вашего проекта

Чтобы следовать примерам кода в этом приложении, загрузите проект Starter и откройте 17-appendix-c-sharing-your-compose-ui-between-android-and-desktop/projects/starter в Android Studio.

Starter — это окончательная версия обучения из главы 14, без приложения для iOS и его кода для конкретной платформы. Он содержит основу проекта, который вы создадите здесь, а Final дает вам возможность сравнить свой код, когда вы закончите.

Чтобы поделиться своим пользовательским интерфейсом, вам нужно создать новый многоплатформенный модуль Kotlin. Это необходимо, потому что разные платформы имеют разные спецификации, а это означает, что вам нужно будет написать код для конкретной платформы. Это похоже на то, что вы делали на протяжении всей этой книги.

Начните с создания новой библиотеки KMM. Вы можете легко сделать это, щелкнув строку состояния Android Studio File , а затем New и New Module .

Затем выберите Kotlin Multiplatform Shared Module и установите:

  • Имя модуля : общий интерфейс
  • Имя пакета : com.raywenderlich.learn.ui
  • Распространение фреймворка iOS : Обычный фреймворк

Нажмите « Готово » и дождитесь синхронизации проекта.

Как видите, в Learn появился новый модуль общего пользовательского интерфейса . Откройте файл settings.gradle.kts , чтобы убедиться, что он добавлен в ваш проект.

Android Studio напрямую поддерживает только KMM (Kotlin Multiplatform Mobile). Итак, когда вы пытаетесь добавить новый модуль и ориентируетесь на другие платформы, например настольные приложения, вам нужно будет вручную добавить эти цели.

Откройте shared-ui и переименуйте папку iosMain в desktopMain .

Теперь откройте общий интерфейс build.gradle.kts и удалите все ссылки на iOS. Начиная с начала этого файла:

  1. Двигаясь к kotlinразделу, удалите все цели iOS:

listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { baseName = "shared-ui" } }

  1. Теперь при sourceSetsудалении всех полей iOS*Mainи :iOS*Test

val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) } val iosX64Test by getting val iosArm64Test by getting val iosSimulatorArm64Test by getting val iosTest by creating { dependsOn(commonTest) iosX64Test.dependsOn(this) iosArm64Test.dependsOn(this) iosSimulatorArm64Test.dependsOn(this) }

Теперь, когда ссылок на iOS больше нет, вернитесь в kotlinраздел и под android()мишенью добавьте:

jvm("desktop")

Это обязательно — иначе вы бы сгенерировали только библиотеку shared-ui для Android.

Наконец, в sourceSetsконфигурации добавьте desktopMainсвойство внизу:

val desktopMain by getting

Синхронизируйте проект. После его завершения посмотрите на структуру проекта. Он должен быть похож на этот:

Рис. C.1 – Иерархия представлений проекта

Рис. C.1 – Иерархия представлений проекта

При создании библиотеки KMM Android Studio также добавляет файл Platform.kt во все папки и файл Greetings.kt внутрь commonMain . Вы можете удалить эти четыре файла. Это всего лишь заполнители, и они не будут использоваться в этом приложении.

Делитесь своим кодом пользовательского интерфейса

Хотя код обеих платформ очень похож, приложение для Android использует библиотеки, зависящие от платформы. Поскольку пользовательский интерфейс должен поддерживаться в обоих случаях, необходимо внести несколько изменений.

Как правило, наиболее распространенный сценарий заключается в том, что у вас есть приложение для Android, созданное с помощью Compose, которое вы хотите перенести на рабочий стол. Итак, вы начнете с перемещения пользовательского интерфейса из androidApp в shared-ui . В конце концов, вы удалите классы, которые больше не нужны, из desktopApp .

Прежде чем вы начнете, есть несколько вещей, которые следует учитывать:

  • Библиотеки Android, использующие собственный SDK, зависят от платформы, поэтому их нельзя использовать в настольных приложениях.
  • shared-ui следует тем же принципам общего модуля, который вы создали ранее: код должен быть полностью написан на Kotlin — даже в его сторонних библиотеках.

С этим пришло время начать свое путешествие. :]

Перенос кода пользовательского интерфейса Android на мультиплатформу

Начните с перемещения всех каталогов внутри androidApp/ui в shared-ui/commonMain/ui . Не перемещайте файл MainActivity.kt , так как действия специфичны для Android.

При появлении запроса о том, как следует выполнить перемещение, выберите «Переместить 8 пакетов в другой пакет», а затем, прежде чем нажимать refactor , убедитесь, что выбраны следующие параметры:

  • Поиск в комментариях и строках.
  • Поиск вхождений текста.

Android Studio откроет новое окно с перечислением нескольких проблем, обнаруженных в ходе этого процесса. Они связаны с ресурсами и библиотеками, которые необходимо добавить в shared-ui . Пока не беспокойтесь об этом. Нажмите Продолжить .

После завершения этой операции переместите каталог компонентов в shared-ui/commonMain . Он должен быть на том же уровне, что и папка ui . При появлении запроса о том, как следует выполнить перемещение, перед нажатием refactor подтвердите, что выбраны следующие параметры:

  • Поиск в комментариях и строках.
  • Поиск вхождений текста.

Нажмите Продолжить .

Примечание . В зависимости от текущего представления, которое вы выбрали для окна структуры проекта слева, вы не сможете перемещать файлы непосредственно в правую папку. Чтобы изменить это, выберите оконный режим Project Files .

Глядя на исходную папку androidApp , можно увидеть только два класса: MainActivity.kt и RWApplication . Все остальные классы пользовательского интерфейса теперь находятся в shared-ui .

Пришло время переместить файлы ресурсов. Начните с создания папки res внутри shared-ui/androidMain :

  1. Щелкните правой кнопкой мыши androidMain .
  2. Выберите Новый каталог .
  3. При появлении запроса выберите res из списка исходных наборов Gradle .

Подобно androidApp/res , эта папка будет содержать все ресурсы приложения, зависящие от платформы. Хотя вы можете переместить все файлы из папки res в это новое место, есть несколько файлов, которыми не обязательно делиться, поскольку они специфичны для Android:

  • ic_launcher_background.xml — фон значка приложения.
  • colors.xml , цвета, используемые для атрибутов XML.
  • themes.xml определяет фон окна.

Переместите все папки, кроме значений, внутри androidApp/res в shared-ui .

Когда ваш код и его ресурсы перемещены в другой модуль, вам необходимо импортировать его в приложение androidApp . В противном случае MainActivity не сможет разрешить свой импорт.

Откройте build.gradle.kts из androidApp и в dependenciesразделе ниже sharedдобавьте:

implementation(project(":shared-ui"))

Синхронизируйте и дождитесь завершения этой операции.

После этого откройте MainActivity.kt . Теперь все импорты должны быть разрешены. Тем не менее, модели представления все еще нуждаются в решении. Поскольку они специфичны для Android, вам необходимо использовать внешнюю библиотеку для их поддержки при нацеливании на многоплатформенность. Подробнее об этом можно прочитать в разделе «Использование LiveData и ViewModel» этой главы.

Написать Мультиплатформенный

Первоначально Jetpack Compose был представлен для Android как новый инструментарий пользовательского интерфейса, в котором можно было, наконец, отказаться от деклараций XML и вызовов findViewById и перейти к новой парадигме — декларативному пользовательскому интерфейсу. Это более лаконичный и современный подход — он отделен от версий API и позволяет быстрее создавать приложения.

Примечание . Вы можете узнать больше о Jetpack Compose для Android в Главе 3 «Разработка пользовательского интерфейса» и в учебных пособиях Jetpack Compose by на сайте raywenderlich.com.

Если вы посмотрите официальную документацию для Jetpack Compose, вы увидите, что на момент написания она состоит из семи библиотек:

  • compose.animation : Анимации, которые вы можете легко использовать.
  • compose.material : Система дизайна материалов для использования в компонентах.
  • compose.material3 : Новейшая версия материального дизайна.
  • compose.foundation : содержит базовые компоновочные объекты — столбец, текст, изображение и т. д.
  • compose.ui : управляет вводом данных, рисованием и макетами.
  • compose.runtime : он не зависит от платформы, что означает, что он не знает, что такое Android или пользовательский интерфейс. Это можно рассматривать как решение для управления деревом.
  • compose.compiler : преобразует @Composableпользовательский интерфейс.

Их можно структурировать в виде следующей схемы высокого уровня:

Compose AnimationCompose Material 3Compose MaterialCompose FoundationCompose UICompose CompilerCompose RuntimeCompose UI Toolkit (Android)

Рис. C.2 Схема высокого уровня Jetpack Compose

Снимок экрана 2022-03-17 в 20.46.42.png

На этом изображении вы можете видеть, что Jetpack Compose можно вставить в:

  • Compose UI Toolkit , зависящий от платформы.
  • Compose Plugins , который содержит среду выполнения Compose и компилятор.

Изменив Compose UI Toolkit, вы сможете использовать Compose на других платформах.

Благодаря Compose Multiplatform компания JetBrains предоставила именно такую ​​поддержку. Это позволяет использовать Compose как для рабочего стола, так и для Интернета. В этой книге вы увидели, как разработать приложение для первой платформы и запустить его на macOS, Windows и Linux.

Различные версии Compose

Настольное приложение уже использует Compose для рабочего стола через подключаемый модуль JetBrains Compose. Чтобы Android и рабочий стол использовали один и тот же пользовательский интерфейс, модуль общего пользовательского интерфейса должен использовать один и тот же, а не версию Android, которую вы используете в androidApp .

Это необходимо, потому что в настоящее время существует ограничение между платформами Android и Multiplatform Compose. Google и JetBrains запускают обновления библиотек в разное время. Это означает, что один из наборов инструментов Compose UI Toolkit может использовать версию, несовместимую с другой. Поскольку вы совместно используете пользовательские интерфейсы для разных приложений, вам необходимо гарантировать, что все работает. Вы можете отслеживать ход решения этой проблемы с помощью Google IssueTracker.

Стоит отметить, что для стабильной работы плагин org.jetbrains.compose заменяет артефакты androidx.compose.* на артефакты от JetBrains. Это временное решение для работы с этими разными версиями.

Переход на мультиплатформу Compose

Откройте файл BookmarkContent.kt из shared-ui . Здесь вы увидите, что импорт в androidx.compose* не разрешается.

Вам нужно добавить плагин Compose Multiplatform и его библиотеки, чтобы решить эту проблему. Откройте файл build.gradle.kts из общего пользовательского интерфейса и импортируйте JetBrain Compose. В pluginsразделе перед com.android.libraryдобавьте:

id("org.jetbrains.compose") version "1.1.0"

Теперь добавьте библиотеки Compose, которые использует проект. Прокрутите вниз до sourceSetsи внутри commonMainраздела добавьте:

dependencies { api(compose.foundation) api(compose.material) api(compose.runtime) api(compose.ui) }

Когда вы закончите, это будет выглядеть так:

val commonMain by getting { dependencies { api(compose.foundation) api(compose.material) api(compose.runtime) api(compose.ui) } }

Синхронизируйте проект и вернитесь к файлу BookmarkContent.kt .

Меньшее количество импортов, выделенных красным цветом, означает, что теперь проект может разрешать свои зависимости Compose.

Обновление общих зависимостей пользовательского интерфейса

Теперь, когда общий пользовательский интерфейс содержит пользовательский интерфейс вашего приложения, пришло время добавить недостающие библиотеки. Откройте файл build.gradle.kts из этого модуля и найдите commonMain/dependencies. Обновите его, чтобы включить:

api(project(":shared")) api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2")

При появлении запроса щелкните, чтобы синхронизировать проект, чтобы он подключался к обеим библиотекам.

Использование сторонних библиотек

Хотя Compose Multiplatform делает первые шаги, сообщество внимательно следит за ней, выпуская библиотеки, которые помогают создать мост между приложениями для Android и настольных компьютеров.

Получение изображений

В приложении для Android вы использовали Coil для извлечения изображений. К сожалению, в настоящее время он не поддерживает Multiplatform , поэтому вы перенесете эту логику на новую: Kamel .

Kamel использует Ktor (подробнее об этой библиотеке вы можете прочитать в Главе 12, «Сеть») для извлечения мультимедиа. Этот API похож на Coil, поэтому вам не нужно будет вносить много изменений.

Откройте файл build.gradle.kts из shared-ui и в commonMain/dependenciesразделе добавьте Kamel:

api(project(":kamel-image"))

Здесь вы используете локальную версию Kamel, поскольку текущая версия не поддерживает последнюю версию Ktor .

Синхронизируйте проект.

В каталоге shared-ui/components откройте ImagePreview.kt . Этот файл содержит логику, необходимую для извлечения изображения из сети, и обрабатывает состояние запроса: успех , загрузка и ошибка .

Composable AddImagePreviewсначала проверяет, urlпусто ли. Если это не так, он создаст requestфайл для загрузки изображения.

Kamel не работает с запросом напрямую в lazyPainterResource, так что вы можете удалить эту логику. Обновите elseпункт на:

else { Box { //1 when (val resource = lazyPainterResource(url)) { //2 is Resource.Loading -> { Logger.d(TAG, "Loading image from uri=$url") AddImagePreviewEmpty(modifier) } //3 is Resource.Success -> { Logger.d(TAG, "Loading successful image from uri=$url") KamelImage( resource = resource, contentScale = ContentScale.Crop, contentDescription = "Image preview", modifier = modifier, crossfade = true ) } //4 is Resource.Failure -> { Logger.d(TAG, "Loading failed image from uri=$url. Reason=${resource.exception}") AddImagePreviewEmpty(modifier) } } } }

Вот пошаговая разбивка этой логики:

  1. lazyPainterResourceявляется частью библиотеки Kamel и похожа на ту request, что была у вас раньше. Он возвращает текущее состояние запроса, через Resource.*которое может быть либо LoadingSuccessлибо Failure.
  2. Обработка состояния запроса, если это loading, это означает, что операция продолжается. Визуально он покажет заполнитель изображения, содержащий логотип приложения.
  3. Если изображение доступно, результатом будет файл successImageВместе с полученным файлом добавляется Composable .
  4. Напротив, если результатом является failure, вы отобразите пустой предварительный просмотр.

Поскольку API для получения изображений перенесен на Kamel, не забудьте удалить все импорты Coil вместе с OptInаннотацией в начале файла. Прокрутите вверх ImagePreview.kt и удалите:

import androidx.compose.ui.platform.LocalContext import coil.annotation.ExperimentalCoilApi import coil.compose.ImagePainter import coil.compose.rememberImagePainter import coil.request.ImageRequest @OptIn(ExperimentalCoilApi::class)

Использование LiveData и ViewModels

Learn был построен с использованием LiveData и ViewModels , которые доступны в Android через библиотеку runtime-livedata . Поскольку он содержит специфичный для Android код, использовать ту же библиотеку в настольном приложении будет невозможно.

К счастью, вокруг Kotlin Multiplatform и Compose существует сильное сообщество, которое пытается сократить разрыв между Android и настольным компьютером и создает библиотеки, которые можно использовать на обеих платформах. Одна из таких библиотек — PreCompose . Он был написан Tlaster и поддерживает компоненты Android Jetpack Lifecycle, ViewModel, LiveData и Navigation в многоплатформенном режиме.

Поскольку на момент написания этой статьи версия на GitHub по-прежнему использует более старую версию Compose Multiplatform вместо импорта опубликованной библиотеки, Learn содержит исходный код проекта с парой изменений — все плагины/библиотеки теперь используют последние версии.

Теперь, когда вы знакомы с precompose , откройте файл build.gradle.kts из модуля shared-ui и после включения api(project())добавьте:

api(project(":precompose"))

Синхронизируйте свой проект. После завершения этой операции вам потребуется обновить ViewModels вашего приложения . Откройте файл BookmarkViewModel.kt в модуле shared-ui и удалите импорт, который вам больше не нужен:

import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.raywenderlich.learn.ui.utils.SingleLiveEvent

Чтобы импортировать ViewModel()и viewModelScope, вам нужно добавить предварительную версию обоих классов:

import moe.tlaster.precompose.viewmodel.ViewModel import moe.tlaster.precompose.viewmodel.viewModelScope

Класс LiveData из этой библиотеки немного отличается от Android. Удалите _itemsпеременную и обновите itemsобъявление до:

val items: MutableState<List<RWEntry>> = mutableStateOf(emptyList())

Это также требует, чтобы вы изменили его использование. Обновите onNewBookmarksList, чтобы установить itemsзначение:

items.value = bookmarks

Теперь откройте FeedViewModel.kt . Вам нужно будет внести аналогичные изменения.

Удалите импорт в библиотеки Android:

import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope

И добавьте те из precompose для ViewModel()и viewModelScope:

import moe.tlaster.precompose.viewmodel.ViewModel import moe.tlaster.precompose.viewmodel.viewModelScope

Наконец, удалите _profileобъявление и замените его profileна:

val profile: MutableState<GravatarEntry> = mutableStateOf(GravatarEntry())

И обновите его использование onMyGravatarDataдо:

profile.value = item

И замените импорт:

import androidx.lifecycle.MutableLiveData

С участием:

import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf

После обновления обеих моделей представления перейдите к androidApp и откройте файл MainActivity.kt . Здесь найдите их объявление и обновите его до:

private lateinit var bookmarkViewModel: BookmarkViewModel private lateinit var feedViewModel: FeedViewModel

Нет поддержки для вызоваby viewModel() precompose . Вместо этого вам нужно инициализировать их внутри функции Composable. Вот почему они установлены как . Внутри добавить:lateinit``setContent

feedViewModel = viewModel { FeedViewModel() } bookmarkViewModel = viewModel { BookmarkViewModel() }

При появлении запроса добавьте:

import moe.tlaster.precompose.ui.viewModel

И переместите fetchвызовы моделей представления ниже его инициализации.

Наконец, удалите вызов observeAsState(), который больше не нужен для объектов LiveData , которые предварительно компонуют использование.

Не забудьте удалить теперь ненужный импорт:

import androidx.activity.viewModels import androidx.compose.runtime.livedata.observeAsState

После внесения обоих изменений удалите класс SingleLiveEvent , который находится в каталоге utils . Это специфично для Android и больше не нужно.

Управление навигацией

Библиотека precompose также поддерживает навигацию Android и рабочего стола между различными экранами. В случае Learn пользователь может переключаться между вкладками на нижней панели навигации.

Настольное приложение уже использует precompose , поэтому вам не нужно ничего делать. Однако Android использовал свои библиотеки, поэтому вам нужно внести здесь несколько изменений. Откройте файл MainActivity.kt внутри androidApp и замените класс, на который расширяется активность:

class MainActivity : PreComposeActivity()

Вам также необходимо удалить androidx.*импорт:

import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity

И добавьте те из precompose :

import moe.tlaster.precompose.lifecycle.PreComposeActivity import moe.tlaster.precompose.lifecycle.setContent

Это на стороне приложения. Теперь вам нужно вернуться к общему модулю пользовательского интерфейса и сделать еще несколько обновлений.

Нижняя панель навигации на Android использует NavHost , который недоступен для мультиплатформы. К счастью, в precompose есть аналогичная функция под названием Navigator . Вам нужно будет заменить текущую реализацию, которая используется, NavHostControllerна эту.

Откройте файл main/MainBottomBar.kt и замените тип NavHostControllerна Navigator. Вам нужно сделать это изменение на MainBottomBarи AppBottomNavigation.

Прокрутите вниз до места BottomNavigationItemопределения и найдите onClick. Обновите эту функцию новой навигацией от Navigator . Замените текущую реализацию на:

if (!isSelected) { selectedIndex.value = index navController.navigate(screen.route) }

Вам не нужно задавать дополнительные настройки.

Как только это будет сделано, не забудьте удалить импорт:

import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController

Теперь, когда вы обновили MainBottomBar , вам необходимо внести аналогичные изменения в MainContent.kt . Откройте этот файл и еще раз замените NavHostControllerтип различных функций на Navigator.

MainScreenNavigationConfigurationsВам также необходимо импортировать NavHostиз precompose , заменить наstartDestination , initialRouteа composableвызов на scene.

Наконец, удалите androidx.*импорт:

import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable

Последнее требуемое изменение находится в файле MainScreen.kt , когда navControllerон определен:

val navController = rememberNavigator()

И удалите:

navController.enableOnBackPressed(false)

Который в настоящее время не поддерживается.

Наконец удалите импорт:

import androidx.navigation.compose.rememberNavController

Миграция библиотек только для JVM на Android

Библиотеки аккомпаниатора , которые использует приложение для Android, создаются только для этой платформы. К счастью, сообщество снова приходит на помощь с версией, поддерживающей рабочий стол. Его портировал пользователь Syer10 , и вы можете найти репозиторий на его GitHub .

Последний выпуск теперь поддерживает как Android, так и JVM, поэтому вы можете легко добавить его, чтобы изучать и делиться им на обеих платформах. Откройте общий интерфейс build.gradle.kts и добавьте в commonMain/dependencies :

api("ca.gosyer:accompanist-pager:0.20.1") api("ca.gosyer:accompanist-pager-indicators:0.20.1")

Синхронизируйте проект.

Хотя вы можете использовать версию Google для Android и версию Syer10 для настольных компьютеров, поскольку вы используете общий пользовательский интерфейс на обеих платформах, вам нужно использовать один и тот же интерфейс на обеих.

Чтобы обеспечить эту поддержку, обе библиотеки , файлы build.gradle.kts , были обновлены с целью Android:

plugins { //1 kotlin("multiplatform") id("org.jetbrains.compose") version "1.1.0" //2 id("com.android.library") } kotlin { //3 android { publishLibraryVariants("release", "debug") } //4 jvm("desktop") { testRuns["test"].executionTask.configure { useJUnitPlatform() } } //5 sourceSets { val commonMain by getting { dependencies { api(compose.material) api(compose.ui) implementation("androidx.annotation:annotation:1.3.0") implementation("io.github.aakira:napier:2.1.0") } } val commonTest by getting val androidMain by getting val androidTest by getting val desktopMain by getting val desktopTest by getting } } //6 android { compileSdk = 31 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") //7 sourceSets["main"].res.srcDirs("src/androidMain/res", "src/commonMain/resources") defaultConfig { minSdk = 21 targetSdk = 31 } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } }

Вот пошаговая разбивка этой логики:

  1. Поскольку вы собираетесь создать библиотеку для более чем одной платформы, вам необходимо включить multiplatformподключаемый модуль.
  2. Кроме того, поскольку одной из этих платформ является Android, вам также необходимо импортировать com.android.library, чтобы вы могли определить конфигурации, установленные на 6.
  3. Раньше эта версия аккомпаниатора просто генерировала версию JVM. Поскольку вы хотите создать версию для Android и настольного компьютера, вам нужно добавить обе цели в kotlinраздел. Здесь вы определяете, что он должен генерировать debugи releaseстроить.
  4. Чтобы легко идентифицировать настольную версию, вы устанавливаете ее имя внутри jvmцели.
  5. Поскольку вы создаете более чем для одной платформы, библиотеки, которые использует проект, должны быть добавлены в файл commonMain dependencies. Хотя одна из библиотек относится к Android, поскольку она не зависит от платформы, вам не нужно определять какие-либо другие библиотеки для других свойств.
  6. Конфигурация, которая будет использоваться для создания сборки Android.
  7. Каталог ресурсов на commonMain будет иметь шрифты и строки приложения , поэтому вам нужно добавить его местоположение в sourceSetsпуть к классу.

Управление ресурсами

Обе платформы обрабатывают ресурсы совершенно по-разному. Android создает класс R во время сборки, который ссылается на все файлы, расположенные в папке res : drawables, strings, colors и т. д. Хотя это дает вам легкий доступ к файлам ресурсов приложения, он не будет работать на другой платформе.

Загрузка локальных изображений

Чтобы загрузить локальные изображения, вам нужно написать эту логику в Kotlin Multiplatform. Это необходимо, так как Android использует класс R для ссылки на изображения, а рабочий стол использует путь к изображению в папке ресурсов .

Также стоит отметить, что обе платформы используют разные форматы изображений. Android использует векторные рисунки, а рабочий стол использует PNG. Имея это в виду, вы не будете делиться этими ресурсами напрямую. Они должны находиться в своих собственных папках для конкретной платформы.

Со стороны Android вы уже скопировали все необходимые ресурсы из androidApp/res . Однако для настольных компьютеров они по-прежнему находятся в desktopApp .

Начните с создания каталога ресурсов внутри shared-ui/desktopMain . Вы можете легко создать его, щелкнув правой кнопкой мыши по этой папке и выбрав Создать ▸ Каталог ▸ ресурсы . Затем переместите папку с изображениями из desktopApp/resources в эту вновь созданную папку.

Со всеми изображениями в правильных папках вам нужно создать класс для ссылки на них. В commonMain/theme создайте Icons.kt и добавьте:

@Composable public expect fun icBrand(): Painter @Composable public expect fun icLauncher(): Painter @Composable public expect fun icMore(): Painter @Composable public expect fun icHome(): Painter @Composable public expect fun icBookmark(): Painter @Composable public expect fun icLatest(): Painter @Composable public expect fun icSearch(): Painter

Эти функции представляют все изображения, которые приложения в настоящее время используют. После того , как объявления ожиданий выполнены, теперь вам нужно написать фактические реализации в androidMain и desktopMain . Начиная с первого, создайте Icons.kt по тому же пути, который вы создали на desktopMain (вам потребуется создать пакет темы ): androidMain/kotlin/com/raywenderlich/shared/ui/theme/Icons. кт

И добавить:

@Composable public actual fun icBrand(): Painter = painterResource(R.drawable.ic_brand) @Composable public actual fun icLauncher(): Painter = painterResource(R.mipmap.ic_launcher) @Composable public actual fun icMore(): Painter = painterResource(R.drawable.ic_more) @Composable public actual fun icHome(): Painter = painterResource(R.drawable.ic_home) @Composable public actual fun icBookmark(): Painter = painterResource(R.drawable.ic_bookmarks) @Composable public actual fun icLatest(): Painter = painterResource(R.drawable.ic_latest) @Composable public actual fun icSearch(): Painter = painterResource(R.drawable.ic_search)

Каждая из этих функций будет обращаться к сгенерированному классу R и обращаться к соответствующей ссылке на рисование или MIP-карту.

Теперь вам нужно сделать то же самое для desktopMain . Создайте тот же файл Icons.kt , но на этот раз в desktopMain/kotlin/com/raywenderlich/shared/ui/theme/ (вам нужно будет снова создать пакет темы ).

Затем добавьте:

@Composable public actual fun icBrand(): Painter = painterResource("images/razerware.png") @Composable public actual fun icLauncher(): Painter = painterResource("images/ic_launcher.png") @Composable public actual fun icMore(): Painter = painterResource("images/ic_more.png") @Composable public actual fun icHome(): Painter = painterResource("images/ic_home.png") @Composable public actual fun icBookmark(): Painter = painterResource("images/ic_bookmarks.png") @Composable public actual fun icLatest(): Painter = painterResource("images/ic_latest.png") @Composable public actual fun icSearch(): Painter = painterResource("images/ic_search.png")

Хотя вы используете painterResourceна обеих платформах, это разные функции. Один из PainterResources.android.kt , а другой PainterResources.desktop.kt .

Со всеми ресурсами, правильно идентифицированными с их соответствующими функциями, вам нужно будет сделать довольно много обновлений, чтобы заменить текущие вызовы класса R этой новой реализацией.

Начиная с алфавита, вам необходимо внести следующие изменения в файлы commonMain :

common/EntryContent В AddEntryContentComposable замените вызов на R.mipmap.ic_launcher:

val icon = icLauncher()

И после этого, когда вы получаете доступ R.drawable.ic_more, с помощью:

val icon = icMore()

И удалить импорт:

import androidx.compose.ui.res.painterResource

компоненты/ImagePreview В AddImagePreviewEmptyComposable замените вызов на R.drawable.ic_brand:

val icon = icBrand()

А затем удалите импорт:

import androidx.compose.ui.res.painterResource

main/BottomNavigationScreens Вам потребуется внести несколько изменений в этот класс. Он больше не может получать @DrawableRes, но вместо этого его нужно установить как @Composable. Это необходимо, поскольку все функции, которые ссылаются на изображения как painterResourceна составные, могут быть вызваны только из другой составной функции.

Замените drawResIdпараметр на:

val icon: @Composable () -> Unit

Поскольку теперь он получает @Composable, вам нужно заменить объекты, объявленные в этом файле:

object Home : BottomNavigationScreens( route = "Home", stringResId = R.string.navigation_home, icon = { Icon( painter = icHome(), contentDescription = R.string.navigation_home ) } ) object Bookmark : BottomNavigationScreens( route = "Bookmark", stringResId = R.string.navigation_bookmark, icon = { Icon( painter = icBookmark(), contentDescription = R.string.navigation_bookmark ) } ) object Latest : BottomNavigationScreens( route = "Latest", stringResId = R.string.navigation_latest, icon = { Icon( painter = icLatest(), contentDescription = R.string.navigation_latest ) } ) object Search : BottomNavigationScreens( route = "Search", stringResId = R.string.navigation_search, icon = { Icon( painter = icSearch(), contentDescription = R.string.navigation_search ) } )

И вы также можете удалить этот импорт:

import androidx.annotation.DrawableRes

И добавить:

import androidx.compose.material.Icon import com.raywenderlich.learn.ui.theme.icBookmark import com.raywenderlich.learn.ui.theme.icHome import com.raywenderlich.learn.ui.theme.icLatest import com.raywenderlich.learn.ui.theme.icSearch

Здесь все еще есть пара ошибок, связанных со строками приложения. Вы увидите, как подробно обновить эту логику, в разделе «Совместное использование строк» ​​этого приложения.

main/MainBottomBar В AppBottomNavigationComposable при определении iconвнутри BottomNavigationItemзамените Iconвызов на:

screen.icon()

И удалите импорт:

import androidx.compose.material.Icon import androidx.compose.ui.res.painterResource

search/SearchContent В AddSearchField Composable замените вызов leadingIconна painterResource:

val icon = icSearch()

И удалить импорт:

import androidx.compose.ui.res.painterResource

Все сделано! Еще пара разделов, и вы получите полностью общий интерфейс вашего приложения.

Использование пользовательских шрифтов

Оба приложения должны использовать шрифт Bitter. Ранее вы переместили все папки из папки androidApp/res в commonMain/resources . Если вы откроете его, вы увидите, что там есть набор битов_*.ttf , представляющих шрифты, которые может использовать ваш текст.

Как и в предыдущем разделе, вам нужно создать многоплатформенную функцию для их загрузки.

Начните с создания файла Font.kt внутри commonMain/theme в модуле shared-ui . Этот класс будет содержать объявление функции:

@Composable expect fun Font(name: String, res: String, weight: FontWeight, style: FontStyle): Font

На шрифт можно ссылаться либо через класс R на Android, либо с помощью его пути на рабочем столе. Аргумент resпредставляет это. weightи styleсоответствуют его свойствам, и, конечно же, nameего названию.

Откройте androidMain/theme и создайте соответствующий файл Font.kt со следующей фактической реализацией:

@Composable actual fun Font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { val context = LocalContext.current val id = context.resources.getIdentifier(res, "font", context.packageName) return Font(id, weight, style) }

Чтобы сделать эту функцию универсальной для настольного приложения, resона должна быть строкой.

Теперь перейдите к desktopMain / theme и добавьте реализацию Font.kt. Создайте файл и добавьте:

@Composable actual fun Font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = androidx.compose.ui.text.platform.Font("font/$res.ttf", weight, style)

Наконец, создайте файл с именем Fonts.kt внутри commonMain/theme и добавьте объект, который будет содержать то, что BitterFontFamilyвы можете использовать:

object Fonts { @Composable fun BitterFontFamily() = FontFamily( Font( "BitterFontFamily", "bitter_bold", FontWeight.Bold, FontStyle.Normal ), Font( "BitterFontFamily", "bitter_extrabold", FontWeight.ExtraBold, FontStyle.Normal ), Font( "BitterFontFamily", "bitter_light", FontWeight.Light, FontStyle.Normal ), Font( "BitterFontFamily", "bitter_regular", FontWeight.Normal, FontStyle.Normal ), Font( "BitterFontFamily", "bitter_semibold", FontWeight.SemiBold, FontStyle.Normal ) ) }

Опять же, Fontсозданные вами классы представляют Composableфункции, и, поскольку вы не можете ссылаться на них вне Composable, вы можете использовать эти шрифты непосредственно из свойства Typography в файле Type.kt.

Перед обновлением всех Text Composable этой новой типографикой вам необходимо удалить ссылки на класс R из Type.kt. Откройте этот файл и удалите:

import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import com.raywenderlich.learn.android.R private val BitterFontFamily = FontFamily( Font(R.font.bitter_bold, FontWeight.Bold), Font(R.font.bitter_extrabold, FontWeight.ExtraBold), Font(R.font.bitter_light, FontWeight.Light), Font(R.font.bitter_regular), Font(R.font.bitter_semibold, FontWeight.SemiBold), )

Теперь, когда больше нет BitterFontFamily, вам нужно удалить этот вызов из всех fontFamilyсвойств. После этого вам необходимо вручную обновить все стили текста , так как невозможно ссылаться на созданные выше шрифты из типографики .

Начиная с commonMain/ui в алфавитном порядке , перейдите к:

  • common/EmptyContent : в Textобъявлении установите fontFamilyаргумент:

fontFamily = Fonts.BitterFontFamily(),

  • common/EntryContent : в AddEntryContentComposable найдите четыре варианта Textиспользования и добавьте:

fontFamily = Fonts.BitterFontFamily(),

  • home/HomeContent : прокрутите вниз до конца этого файла и Textдобавьте:

fontFamily = Fonts.BitterFontFamily(),

  • home/HomeSheetContent : найдите два Textвызова и добавьте:

fontFamily = Fonts.BitterFontFamily(),

  • last/LatestContent : установите fontFamilyв Textобъявлениях AddNewPageи AddNewPageEntry:

fontFamily = Fonts.BitterFontFamily(),

  • main/MainBottomBar : при определении при BottomNavigationItemдобавлении Text:

fontFamily = Fonts.BitterFontFamily(),

  • main/MainTopAppBar : обновите, Textчтобы он содержал fontFamilyаргумент:

fontFamily = Fonts.BitterFontFamily(),

  • search/SearchContent : Наконец, при определении placeholderнабора fontFamilyin Text:

fontFamily = Fonts.BitterFontFamily(),

Совместное использование строк

В настоящее время нет прямой поддержки этой функции. Это правда, что вы можете следовать подходу, аналогичному тому, который вы использовали для обмена локальными изображениями. Однако это потребует много времени и денег в обслуживании. Для каждой новой строки нужно создать три разные функции (одну общую и две на уровне платформы).

К счастью, команда разработчиков IceRock создала библиотеку moko-resources , которая поддерживает именно это и многое другое!

Начните с открытия файла build.gradle.kts , расположенного в корневом каталоге. В buildscriptразделе, внутри dependencies, в конце списка добавить:

classpath("dev.icerock.moko:resources-generator:0.18.0")

Это добавит resources-generatorв проект. Теперь откройте файл build.gradle.kts , но на этот раз файл из shared-ui и добавьте его плагин:

id("dev.icerock.mobile.multiplatform-resources")

При этом вам необходимо установить имя пакета приложения для использования moko-resources . После pluginsобъявления добавить:

multiplatformResources { multiplatformResourcesPackage = "com.raywenderlich.learn" }

Теперь вам нужно добавить библиотеку на commonMain/dependenciesраздел:

api("dev.icerock.moko:resources:0.18.0")

Нажмите «Синхронизировать» и подождите, пока проект загрузит эту новую библиотеку.

Строки в настольном приложении в настоящее время жестко запрограммированы. Этого достаточно для простого приложения, но если вы продолжаете добавлять новые функции, использующие строки, их будет проще поддерживать в одном файле. Более того, если вы хотите добавить поддержку интернационализации, вам потребуется несколько строковых файлов, чтобы ОС могла знать, какой из них следует загрузить.

Вы будете повторно использовать файл Android strings.xml в качестве общих строк на обеих платформах. Создайте папку ресурсов внутри shared-ui/commonMain , щелкнув правой кнопкой мыши commonMain и выбрав New ▸ Directory ▸ resources при появлении запроса.

Чтобы moko-resources работал, файлы строк должны находиться по определенному пути: commonMain/resources/MR/base . Создайте эти два каталога и переместите strings.xml из androidApp/res в это новое место.

Примечание . Если ваше приложение поддерживает интернационализацию, вам следует создать папку внутри MR с кодом страны языка, а затем переместить в это место соответствующий файл strings.xml .

Создайте проект. moko-resources сгенерирует несколько мультиплатформенных файлов (Android, рабочий стол и обычный), содержащих строки, которые будет использовать ваше приложение. Вы можете найти их по адресу:

  • общий пользовательский интерфейс/сборка/сгенерированный/моко/

Прежде чем использовать любую из этих строк, не хватает одного шага: вам все еще нужно реализовать логику для доступа к ним.

Начните с создания файла Resources.kt в папке commonMain/utils в модуле shared-ui :

expect fun getString(resId: StringResource): String

Теперь вам нужно объявить фактические реализации как для Android, так и для рабочего стола. Начиная с первого, перейдите в androidMain/ui/utils и создайте соответствующий файл Resources.kt с:

actual fun getString(resId: StringResource): String { return StringDesc.Resource(resId).toString(appContext) }

Соответствует контекстуappContext приложения , который уже объявлен в файле PlatformDatabase.kt и установлен в RWApplication.kt .

На desktopMain/ui/utils создайте оставшийся файл Resources.kt и добавьте:

actual fun getString(resId: StringResource): String { return StringDesc.Resource(resId).localized() }

После определения реализации вам нужно будет пройти через все классы и обновить ссылки на класс R. Вместо доступа к R.string.* они будут вызывать getStringфункцию, которую вы только что создали.

Начиная с commonMain/ui в алфавитном порядке , перейдите к:

  • bookmark/BookmarkContent : BookmarkContentв Composable обновите stringResourceвызов до:

text = getString(MR.strings.empty_screen_bookmarks)

И удалите импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

  • common/EntryContent.kt : Найдите все вызовы stringResourceи последовательно обновите их до эквивалента getString(). Сверху обновите descriptionзначение до:

val description = getString(MR.strings.description_feed_icon)

Затем замените ссылку на app_ray_wenderlichна:

text = getString(MR.strings.app_ray_wenderlich),

Наконец, измените доступ description_moreк:

val description = getString(MR.strings.description_more)

Еще раз удалите импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

  • компоненты/ImagePreview .kt : вам нужно внести только одно изменение. Прокрутите вниз AddImagePreviewEmptyи обновите descriptionсвойство, которое обращается к классу R , на:

val description = getString(MR.strings.description_preview_error)

И удалите неиспользуемый импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

  • home/HomeSheetContent.kt : Найдите доступ к классу R. Первый является результатом условия if, используемого для определения того, что textследует отображать. Замените этот блок кода на:

val text = if (item.value.bookmarked) { getString(MR.strings.action_remove_bookmarks) } else { getString(MR.strings.action_add_bookmarks) }

После этого обновления типом textсвойства является String , поэтому вы можете удалить вызов stringResourceиз TextComposable ниже:

text = text

В конце файла есть еще одна ссылка на R. Замените этот вызов на:

text = getString(MR.strings.action_share_link),

И удалите импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

  • last/LatestContent.kt : LatestContentв Composable обновите вызов строк до:

AddEmptyScreen(getString(MR.strings.empty_screen_loading))

Как всегда, удалите ненужный импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

  • main/BottomNavigationScreens.kt : @StringResссылка на строку, характерная для платформы Android. Поскольку вы используете этот класс совместно с настольным приложением, вам необходимо обновить этот параметр до общего типа — String . Изменить stringResIdна:

val title: String,

При этом вам необходимо обновить все объекты, объявленные в этом классе.

Для homeобъекта обновите stringResIdи contentDescription, соответственно, до:

title = getString(MR.strings.navigation_home),

contentDescription = getString(MR.strings.navigation_home)

То же самое относится и к bookmarkобъекту:

title = getString(MR.strings.navigation_bookmark),

contentDescription = getString(MR.strings.navigation_bookmark)

И к latest:

title = getString(MR.strings.navigation_latest),

contentDescription = getString(MR.strings.navigation_latest)

Наконец, для search:

title = getString(MR.strings.navigation_search),

contentDescription = getString(MR.strings.navigation_search)

Удалите ненужный теперь импорт:

import androidx.annotation.StringRes import com.raywenderlich.learn.android.R

  • main/MainBottomBar.ktBottomNavigationItem : с предыдущим изменением вам необходимо обновить файл MainBottomBar. Замените stringResourceвызов на:

text = screen.title,

И удалите его импорт:

import androidx.compose.ui.res.stringResource

  • main/MainTopAppBar.kt : Замените stringResourceвызов на:

text = getString(MR.strings.app_name),

И удалите импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

  • search/SearchContent.kt : Это последний файл, который нужно обновить! Прокрутите вниз AddSearchFieldи найдите два вызова stringResource. В первом вы определяете placeholderи должны быть обновлены до:

text = getString(MR.strings.search_hint),

Второй для leadingIcon, и вам нужно изменить на description:

val description = getString(MR.strings.description_search)

И, как всегда, удалите импорт:

import androidx.compose.ui.res.stringResource import com.raywenderlich.learn.android.R

Чего не хватает?

Сделав все эти изменения, вы почти закончили. Откройте проект desktopApp и:

  • Удалите пользовательский интерфейс и компоненты , кроме файла Toast.kt .
  • Переместите папку шрифтов из ресурсов в commonMain/desktopMain/resources .
  • Откройте класс Utils.kt и удалите SupressLintаннотацию, специфичную для Android.

Вы должны оставить только Main.kt , который является точкой входа вашего приложения.

Здесь обновите MainScreenвызов:

MainScreen( feeds = items, bookmarks = bookmarks, onUpdateBookmark = { updateBookmark(it) }, onShareAsLink = {}, onOpenEntry = { openLink(it) } )

Теперь откройте его build.gradle.kts и включите зависимость общего пользовательского интерфейса , которую вы создали в этом приложении. Чтобы избежать ненужных реализаций, вы можете заменить все библиотеки в этом разделе на:

implementation(project(":shared")) implementation(project(":shared-ui")) implementation(project(":shared-action")) implementation(compose.desktop.currentOs)

Сделайте то же самое для androidApp . Откройте его build.gradle.kts и замените dependenciesраздел на:

dependencies { implementation(project(":shared")) implementation(project(":shared-ui")) implementation(project(":shared-action")) implementation("com.google.android.material:material:1.5.0") }

Синхронизируйте свой проект и, наконец, скомпилируйте и запустите приложения для ПК и Android. Вы увидите такие экраны:

Рис. C.3 Лента в приложении для Android

Рис. C.3 Лента в приложении для Android

Рис. C.4 Лента в настольном приложении

Рис. C.4 Лента в настольном приложении

Куда пойти отсюда?

Поздравляем! Вы только что закончили Kotlin Multiplatform. Какая поездка! Из этой книги вы узнали, как совместно использовать бизнес-логику приложения на разных платформах: Android, iOS и настольных компьютерах. Теперь, когда вы освоили KMP, возможно, вам интересно узнать больше о Jetpack Compose и SwiftUI . Эти книги — идеальная отправная точка!