KA0711 — Сериализация (Serialization)

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

Отличная работа по завершению первых двух разделов книги! Ты отлично справляешься. Теперь, когда вы знакомы с Kotlin Multiplatform, у вас есть все необходимое для решения задач последнего раздела.

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

Learn использует RSS-каналы raywenderlich.com, чтобы показывать последние учебные пособия для Android, iOS, Unity и Flutter. Вы можете добавить их в список для чтения позже, поделиться ими с другом, выполнить поиск определенного ключа или просто просмотреть все, что выпустила команда. Он будет иметь тот же внешний вид, к которому вы уже привыкли на веб-сайте raywenderlich.com.

Необходимость сериализации

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

Существуют разные типы форматов сериализации — например, JSON или потоки байтов. Вы прочтете об этом больше в Главе 12, «Сеть», когда будете рассматривать сетевые запросы.

Android использует эту концепцию для обмена данными между действиями, службами или получателями — либо в том же приложении, либо в сторонних приложениях. Разница в том, что вместо того, чтобы полагаться на Serializableотправку данных из пользовательских типов, ОС требует, чтобы вы реализовали Parcelableотправку этих объектов.

Обзор проекта

Чтобы следовать примерам кода в этом разделе, загрузите начальный проект и откройте 11-serialization/projects/starter с помощью Android Studio.

Примечание . В этом проекте используется Jetpack Compose 1.2.0-alpha01 и Multiplatform Compose 1.1.0 с Kotlin 1.6.10. Для успешной сборки и запуска приложения используйте Android Studio Bumblebee 2021.1.1 Patch 2 или более новую версию.

В Starter есть скелет приложения, которое вы создадите, а Final дает вам что-то, с чем можно сравнить ваш код, когда вы закончите.

После синхронизации проекта вы увидите набор подпапок и других важных файлов:

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

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

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

Android-приложение

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

  • компоненты : общие составные функции, которые представляют определенную цель и используются на разных экранах.
  • ui : содержит весь пользовательский интерфейс. Подпакеты соответствуют определенным экранам ( закладки , домашний , последний или поиск ), панели навигации ( основной ), системе разработки приложений ( тема ) или служебным классам (утилита ) .
  • RWApplication.kt : класс Application , который инициализирует контекст , необходимый SQLDelight .

Создайте и запустите приложение.

Рис. 11.2 — Работа стартового проекта Android. Пустой экран без данных.

Рис. 11.2 — Работа стартового проекта Android. Пустой экран без данных.

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

iOS-приложение

Ваше приложение для iOS находится в папке iOSApp . Перейдите в эту папку и в корневой каталог и введите:

pod install

Это позволяет получить все зависимости проекта.

После этого пришло время открыть проект. Используйте Xcode или AppCode, чтобы открыть файл iosApp.xcworkspace , расположенный в папке iosApp .

Примечание . Существует два разных файла iosApp.* : xcworkspace , созданный CocoaPods, и xcodeproj , созданный Xcode. Вам нужно открыть первый, чтобы IDE могла правильно разрешить зависимости проекта.

Вам не нужно никаких дополнительных шагов для запуска приложения. Откройте цель iosApp , перейдите на вкладку « Фазы сборки » и выберите раскрывающийся список « Выполнить сценарий» . Здесь у вас есть:

cd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode

Эта задача автоматически компилирует платформу SharedKit и при необходимости добавляет ее в ваш проект.

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

Рис. 11.3 — Работа стартового проекта iOS. Пустой экран без данных.

Рис. 11.3 — Работа стартового проекта iOS. Пустой экран без данных.

Настольное приложение

Настольное приложение похоже на приложение для Android. Код был скопирован из одного проекта в другой с небольшими изменениями, а именно в используемых библиотеках, которые не были доступны для цели JVM :

  • Kamel : библиотека загрузки изображений.
  • precompose : библиотека сообщества, позволяющая использовать Jetpack Lifecycle , ViewModel , LiveData и Navigation в настольном приложении.

Хотя эти библиотеки доступны по указанным выше ссылкам, они не обновлены до последних версий Kotlin или Compose. Вот почему они включены в этот проект. Дополнительную информацию об этом можно найти в Приложении C.

Чтобы запустить настольное приложение, перейдите в командную строку и в корневой папке проекта введите:

./gradlew desktopApp:run

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

Рис. 11.4 — Работающий стартовый проект рабочего стола. Пустой экран без данных.

Рис. 11.4 — Работающий стартовый проект рабочего стола. Пустой экран без данных.

Примечание . Используйте мышь для изменения размера окна.

Общий модуль

Это содержит всю бизнес-логику Learn . Это многоплатформенный код, который используется на Android, iOS и настольных компьютерах.

Ты найдешь:

  • androidMain : определение кода для платформы Android.
  • androidTest : тесты, написанные для упомянутого выше кода для конкретной платформы.
  • commonMain : содержит бизнес-логику, которая будет использоваться на всех целевых платформах.
  • commonTest : содержит все тесты, не зависящие от платформы, созданные для проверки того, работает ли бизнес-логика должным образом.
  • iosMain : определение кода для платформы iOS.
  • iostTest : тесты, написанные для упомянутого выше кода для конкретной платформы.

Общий код

Когда вы откроете общий модуль, вы увидите две вещи внутри commonMain :

  • kotlin : Где находится бизнес-логика приложения.
  • sqldelight : содержит файл SQL, который будет использоваться SQLDelight для создания базы данных приложения, а также соответствующие методы CRUD для взаимодействия с ней.

Этот модуль следует парадигме чистой архитектуры, группируя файлы в соответствии с их ответственностью в бизнес-логике. Откройте каталог kotlin , и вы увидите:

  • data : сетевой уровень общего модуля. Извлекает каналы RW и определяет модель данных для каждой записи RSS.
  • domain : десериализует ответ и создает список каналов, которые позже могут быть использованы пользовательским интерфейсом. Сохраняет данные в базу данных и определяет обратные вызовы, которые будут использоваться для уведомления о появлении новых данных.
  • платформа : объявляет, какие функции необходимо реализовать на каждой платформе. Он представляет код приложения для конкретной платформы, и именно здесь вы найдете ожидаемое ключевое слово.
  • презентация : этот слой создает мост между пользовательским интерфейсом и логикой приложения.
  • ServiceLocator : одноэлементный объект , который обеспечивает доступ к различным модулям в приложении. Он также отвечает за инициализацию всех презентаторов вместе со свойствами, которые им требуются.

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

Возможности приложения

Прежде чем приступить к написанию кода, взгляните сначала на концепцию приложения и его функции:

Рис. 11.5 – Обзор экранов приложений и навигация.

Рис. 11.5 – Обзор экранов приложений и навигация.

Не беспокойтесь о деталях на каждом экране. У вас будет возможность увидеть их поближе в следующих главах.

Learn имеет четыре разных экрана, на которые можно перейти из нижней панели приложения:

Дома

Это экран приложения по умолчанию. Он показывает горизонтальный список со всеми темами raywenderlich.com и списком последних опубликованных статей.

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

Чтобы открыть один, нажмите на карточку. Вы будете автоматически перенаправлены в браузер, где сможете получить к нему доступ и прочитать его. Если вместо этого вы нажмете на три точки внутри карточки, приложение отобразит нижний лист. Оттуда вы можете добавить его в свой список для чтения позже, доступный на экране закладок , или отправить его другу, чтобы он мог быть в курсе последних статей.

Закладки

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

Самый последний

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

Поиск

Там много контента, который вы можете просмотреть. Здесь вы можете отфильтровать по определенному ключевому слову и, наконец, найти ту статью, которую искали.

Теперь, когда вы знакомы с приложением и проектом, пришло время научиться реализовывать эти функции.

Добавление сериализации в конфигурацию Gradle

Kotlin не поддерживает сериализацию и десериализацию непосредственно на уровне языка. Вам нужно будет либо реализовать все с нуля, либо использовать библиотеку, которая уже предоставляет вам эту поддержку. Более того, поскольку вы разрабатываете для мультиплатформы, важно помнить, что у вас не может быть Java-кода в commonMain . В противном случае проект не скомпилируется. Он должен быть написан на Kotlin для работы на разных платформах, на которые вы ориентируетесь: Android, Native и JVM.

Нет, нет необходимости реализовывать эту поддержку. Он уже доступен в kotlinx.serialization — библиотеке, созданной и поддерживаемой JetBrains.

Примечание : Подробнее о kotlinx.serialization читайте в официальной документации . Или погрузитесь в исходный код, доступный в его официальном репозитории .

Пришло время импортировать эту библиотеку в приложение. Откройте Android Studio и дождитесь завершения синхронизации проекта. В корневой папке проекта есть файл build.gradle.kts ; откройте его и добавьте следующее classpath:

classpath("org.jetbrains.kotlin:kotlin-serialization:1.6.10")

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

Теперь откройте файл build.gradle.kts внутри общего доступа и загрузите плагин сериализации, добавив следующий код внутри pluginsблока:

kotlin("plugin.serialization")

Синхронизируйте и дождитесь завершения этого процесса. Когда все будет готово, система загрузит библиотеку и добавит ее в проект.

В проекте есть четыре разных файла build.gradle.kts :

  • build.gradle.kts : находится в корневой папке проекта и настраивает как приложение Android, так и общий модуль. Он содержит репозитории, из которых будут загружены зависимости.
  • shared/build.gradle.kts : это файл конфигурации общего модуля. Он содержит подключаемые модули, которые будут использоваться, библиотеки, а также платформы, на которые вы собираетесь ориентироваться.
  • androidApp/build.gradle.kts : файл конфигурации для приложения Android. Определяет набор параметров, используемых для компиляции проекта, а именно версию SDK, зависимости и флаги компиляции.
  • desktopApp/build.gradle.kts : это файл конфигурации для настольного приложения. Он определяет ту же конфигурацию, что и файл Android build.gradle.kts .

Различные форматы сериализации

kotlinx.serialization нестандартно поддерживает набор форматов сериализации:

  • JSON : облегченный удобочитаемый формат обмена данными. Learn использует их для загрузки ссылок RSS-канала RW из локального файла, а затем для десериализации данных, полученных с сервера ( kotlinx-serialization-json ).
  • Буферы протоколов : кроссплатформенный механизм для сериализованных структурированных данных ( kotlinx-serialization-protobuf ).
  • CBOR : формат сериализации двоичных данных ( kotlinx-serialization-cbor ).
  • Свойства : файл «ключ-значение», в котором сохраняются параметры конфигурации приложения, используемого в технологиях, связанных с Java. В разработке для Android у вас есть файл с именем gradle.properties , который содержит свойства для настройки вашего демона Gradle/вашего приложения во время компиляции ( kotlinx-serialization-properties ).
  • HOCON : надмножество JSON, которое более удобочитаемо и обычно используется в файлах конфигурации ( kotlinx-serialization-hocon ).

Примечание . Существуют также две поддерживаемые сообществом библиотеки для YAML и Apache Avro .

За исключением kotlinx-serialization-json , все эти библиотеки пока экспериментальные. Хотя все они кажутся надежными и используются в ряде приложений, стоит упомянуть, что API может (значительно) измениться в будущих выпусках, а это означает, что вы можете в конечном итоге реорганизовать его использование.

В рамках этой книги вам нужно будет только добавить kotlinx-serialization-json . Перейдите к shared , откройте файл build.gradle.kts и найдите commonMainполе. Набор уже есть dependenciesна проекте. В конце этого списка добавьте:

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")

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

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

Если вы используете пользовательские типы для своих объектов, сериализатор может быть недоступен. Вам нужно будет создать свой собственный, иначе сериализация/десериализация не будет работать.

Хорошим примером этого является класс данных RWContent.kt внутри общего модуля. Тип platformполя — PLATFORMперечисление, созданное для определения того, к какому разделу относится статья.

Если вы не создадите собственный сериализатор, kotlinx-serialization-json не сможет определить ключевые слова «Все», «Android», «iOS», «Unity» или «Flutter». Это происходит потому, что атрибут в JSON имеет тип String, а должен быть PLATFORMвместо него. Для обеспечения этой поддержки необходимо реализовать собственный сериализатор/десериализатор.

В пакете commonMain внутри общего модуля перейдите к данным и создайте файл RWSerializer.kt .

Начните с добавления findByKeyметода:

private fun findByKey(key: String, default: PLATFORM = PLATFORM.ALL): PLATFORM { return PLATFORM.values().find { it.value == key } ?: default }

Это получит keyи вернет соответствующее значение в перечислении PLATFORM.

Теперь, когда вы добавили функцию сопоставления, создайте RWSerializerобъект. В этот же файл добавьте:

//1 @OptIn(ExperimentalSerializationApi::class) //2 @Serializer(forClass = PLATFORM::class) //3 object RWSerializer : KSerializer<PLATFORM> { //4 override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PLATFORM", PrimitiveKind.STRING) //5 override fun serialize(encoder: Encoder, value: PLATFORM) { encoder.encodeString(value.value) } //6 override fun deserialize(decoder: Decoder): PLATFORM { return try { val key = decoder.decodeString() findByKey(key) } catch (e: IllegalArgumentException) { PLATFORM.ALL } } }

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

  1. API сериализации все еще является экспериментальным. Каждый раз, когда вы его используете, Android Studio автоматически подчеркивает его вызов, предлагая либо использовать, OptInлибо Requireаннотировать. Эта ошибка отображается для уведомления разработчика о том, что этот API может измениться в будущем. В этом проекте вы будете использовать OptIn, чтобы избежать добавления новой аннотации при каждом вызове этого класса.
  2. Свяжите RWSerializerс определенным классом. В этом сценарии он соответствует enum PLATFORM.
  3. Вам нужно будет расширить KSerializerкласс и определить тип объекта, который будет сериализован/десериализован.
  4. Необходимо определить, PrimitiveSerialDescriptiorчто содержит имя класса — PLATFORM— и как параметр должен быть прочитан. В данном случае это будет файл String.
  5. Теперь, когда все готово, пришло время определить serializeметод. Поскольку тип содержимого PLATFORMString, вам просто нужно вызвать encodeStringи отправить valueполученный объект.
  6. Наконец, для десериализации вы вызовете противоположный метод, то есть decodeString, и с необработанным значением (ключом) вы вызовете, findByKeyчтобы увидеть, какому значению перечисления оно соответствует.

Вот и все! RWSerializerготов. Вам просто нужно добавить его в класс. Снова откройте файл RWContent.kt и над объявлением PLATFORMдобавьте:

@Serializable(with = RWSerializer::class)

Это связывает новый сериализатор устройства с PLATFORMклассом. Не забудьте импортировать из kotlinx.serialization.Serializable.

Сериализация/десериализация новых данных

Перейдите к FeedPresenter.kt внутри commonMain/presentation , и вы увидите свойство RW_CONTENT . Этот JSON содержит всю необходимую информацию для создания верхнего горизонтального списка приложения на главном экране. Его структура имеет следующие атрибуты:

  • платформа : различные области, охватываемые статьями raywenderlich.com: Android, iOS, Unity и Flutter. Есть пятое значение — все. После установки он удаляет этот фильтр и показывает все опубликованные.
  • url : содержит URL-адрес RSS-канала, откуда следует получать статьи.
  • image : изображение обложки, соответствующее platformвыбранному.

Эти три атрибута уже сопоставлены с классом данных RWContent.kt , который находится внутри data/model . Откройте его и добавьте в начало объявления:

@Serializable

Класс RWContentданных использует @Serializable, поэтому при декодировании свойства RW_CONTENT он легко генерирует список RWContent , который сопоставляет атрибуты строки JSON с полями в классе данных.

Когда все определено, перейдите к файлу FeedPresenter.kt внутри пакета презентации и сначала добавьте в класс:

private val json = Json { ignoreUnknownKeys = true }

Это создаст Jsonобъект, который будет использоваться для декодирования содержимого файла. Важно установить ignoreUnknownKeys, чтобы trueизбежать каких-либо исключений, которые могут быть вызваны в случае, если одно из полей внутри RWContent.kt не имеет прямого атрибута в файле JSON. Не забудьте импортировать kotlinx.serialization.json.Json.

Теперь обновите content свойство, чтобы декодировать RW_CONTENTвместо возврата emptyList:

val content: List<RWContent> by lazy { json.decodeFromString(RW_CONTENT) }

contentлениво инициализируется. Другими словами, он откроет файл и прочитает его содержимое только при доступе. Когда это сделано, он вызывает decodeFromStringдля создания списка RWContentобъектов.

Добавьте следующий импорт для разрешения decodeFromString:

import kotlinx.serialization.decodeFromString

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

Рис. 11.6 — Android-приложение для разных платформ

Рис. 11.6 — Android-приложение для разных платформ

Рис. 11.7 — iOS-приложение с разными платформами

Рис. 11.7 — iOS-приложение с разными платформами

Рис. 11.8 — Десктопное приложение с разными платформами

Рис. 11.8 — Десктопное приложение с разными платформами

Сериализуемый против Parcelable

Java имеет Serializableинтерфейс, расположенный в пакете java.io. Он использует отражение для чтения полей из объекта, что является медленным процессом, который часто создает множество временных объектов, которые влияют на объем памяти приложения.

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

Кто-то может возразить, что Parcelableэто сложнее реализовать. Это было верно несколько лет назад, поскольку необходимо было переопределить пару методов и создать методы чтения/записи в соответствии с полями объекта. Но вам не нужно делать все это сейчас, так как плагин kotlin-parcelize генерирует этот код автоматически. Таким образом, единственное усилие здесь — добавить аннотацию в начало класса — @Parcelize— и расширить Parcelable.

Реализация Parcelize в KMP

Parcelableи Parcelizeпредставляют собой набор классов, специфичных для платформы Android. Общий модуль содержит бизнес-логику приложения и его модели данных, которые используются на нескольких платформах. Поскольку этот код должен быть специфичным для платформы, вам нужно будет объявить его, используя ключевые слова expect и fact , которые вы уже использовали в главе 1, «Введение».

Parcelizeявляется частью плагина kotlin-parcelize , который содержит Parcelableгенератор кода. Прежде чем писать объявление Android для этого класса, вам нужно сначала добавить его в файл build.gradle.kts общего модуля . Откройте его, и в разделе добавьте:plugin

id("kotlin-parcelize")

Синхронизируйте проект и добавьте эту новую зависимость.

Чтобы установить класс данных как Parcelable , нужно добавить аннотацию @Parcelizeв начало объявления класса, а затем расширить Parcelableгенератор. Должно быть что-то похожее на:

import kotlinx.parcelize.Parcelize @Parcelize data class RWEntry(val id: String, val entry: String): Parcelable

Однако, поскольку этот специфичный для платформы код не должен существовать в commonMain , вам необходимо определить это поведение на уровне платформы — в данном случае в androidMain .

Определение ожидаемого/фактического целевого значения для Android

Как правило, имена классов, зависящих от платформы, начинаются с префикса Platform-. Это улучшает читаемость, упрощая идентификацию этих классов без необходимости перемещаться по всем пакетам, чтобы найти их.

Чтобы реализовать Parcelize , вам нужно объявить:

  • Класс Parcelable , который расширяется классом данных.
  • Аннотация Parcelize , которая используется для активации плагина kotlin-parcelize .

Перейдите к commonMain в общем модуле, перейдите к платформе и создайте файл PlatformParcelable.kt . Откройте его и добавьте:

package com.raywenderlich.learn.platform //1 expect interface Parcelable //2 @OptIn(ExperimentalMultiplatform::class) @OptionalExpectation //3 @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) //4 expect annotation class Parcelize()

Разобьем этот фрагмент кода на части:

  1. Интерфейс Parcelable , который необходимо определить.
  2. Объявление аннотации все еще экспериментальной, а это означает, что API может измениться в будущем. Эти две аннотации уведомляют пользователя об этом поведении и не позволяют Android Studio предупреждать разработчика каждый раз при вызове этого Parcelizeкласса.
  3. Targetи Retentionаннотации являются частью Parcelizeаннотации Котлина. Чтобы сохранить то же поведение, что и нативное, они также добавлены сюда.
  4. ParcelizeАннотация, которая будет определена .

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

Этот класс можно использовать только с аргументом компилятора ‘-opt-in=kotlin.RequiresOptIn’

Они печатаются, чтобы предупредить разработчика о том, что некоторые функции OptInмогут измениться или стать несовместимыми в будущем. Это то, с чем вам нужно быть осторожным, поскольку это означает, что вы можете в конечном итоге реорганизовать свой код, если он изменится. В любом случае, после подтверждения вы всегда можете удалить эти сообщения, добавив аргумент компилятора: -opt-in=kotlin.RequiresOptIn.

Откройте общий файл build.gradle.kts и добавьте следующий код в конец этого файла:

kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") }

В зависимости от используемых вами классов данных вам может потребоваться создать ожидаемый / фактический класс для других аннотаций. Одним из таких примеров является @RawValue, который используется вместе с сериализаторами по умолчанию для пользовательских типов. Вы можете использовать тот же подход, что и в Parcelize , для достижения той же цели:

@OptIn(ExperimentalMultiplatform::class) @OptionalExpectation @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.BINARY) expect annotation class RawValue()

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

Перейдите в каталог платформы и создайте соответствующий файл PlatformParcelable.kt . Откройте его и добавьте:

package com.raywenderlich.learn.platform actual typealias Parcelable = android.os.Parcelable actual typealias Parcelize = kotlinx.android.parcel.Parcelize

Так как Parcelableони Parcelizeуже существуют на платформе Android, создавать их не нужно. Вместо этого используйте typealiasдля создания ссылки между ожидаемым классом и самим фактическим классом. Другими словами, это как если бы вы вызывали android.os.Parcelable или kotlinx.android.parcel.Parcelize напрямую.

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

Работа с целями, которые не поддерживают Parcelize

Теперь, когда вы внедрили Parcelize на common и Android, если вы посмотрите на actualполя, которые вы добавили, вы увидите красную подчеркивание, что означает, что что-то не так. Это происходит потому, что приложение нацелено на другие платформы, на которых отсутствует actualреализация.

Перейдите в папку iosMain и внутри каталога платформы создайте соответствующий файл PlatformParcelable.kt .

Примечание . Нажмите Alt + Enter , и Android Studio автоматически предложит создать файлы для остальных платформ. Сгенерируйте эти файлы, нажав OK .

Откройте PlatformParcelable.kt и добавьте:

package com.raywenderlich.learn.platform actual interface Parcelable

Вот и все! iOS не имеет соответствующего интерфейса Parcelable , поэтому определите пустое объявление. Компилятор автоматически удаляет и аннотацию, и класс, так как нет набора объявлений. Сгенерированная структура не содержит ссылок на Parcelizeи Parcelable.

Добавьте тот же файл в desktopMain/platform .

Это ожидаемое поведение, так как это специфичная функция только для Android.

Добавление Parcelize к существующим классам

Когда все настроено, перейдите в commonMain и найдите классы данных RWContent и RWEntry . Они находятся внутри пакета данных/модели .

В каждом из этих файлов добавьте @Parcelizeаннотацию над объявлением класса и расширьте Parcelable:

Файл RWContent.kt будет выглядеть так:

@Parcelize @Serializable data class RWContent( val platform: PLATFORM, val url: String, val image: String ) : Parcelable

И RWEntry.kt будет выглядеть так:

@Parcelize data class RWEntry( val id: String = "", val link: String = "", val title: String = "", val summary: String = "", val updated: String = "", val imageUrl: String = "", val platform: PLATFORM = PLATFORM.ALL, val bookmarked: Boolean = false ) : Parcelable

Теперь вы можете без проблем отправлять этот объект по разным действиям.

Тестирование

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

Тестирование сериализации

Чтобы протестировать свой код, вам нужно перейти к общему модулю, щелкнуть правой кнопкой мыши папку src и выбрать « Создать ▸ Каталог » . В раскрывающемся списке выберите commonTest/kotlin . Здесь создайте класс SerializationTests.kt :

class SerializationTests { }

Вам потребуется создать тесты кодирования и декодирования, чтобы убедиться, что все работает должным образом. Начните с написания кодировщика. Добавьте в класс следующий метод:

@Test fun testEncodePlatformAll() { val data = RWContent( platform = PLATFORM.ALL, url = "https://www.raywenderlich.com/feed.xml", image = "https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png" ) val decoded = Json.encodeToString(RWContent.serializer(), data) val content = "{\"platform\":\"all\",\"url\":\"https://www.raywenderlich.com/feed.xml\",\"image\":\"https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png\"}" assertEquals(content, decoded) }

Здесь вы подтверждаете, что ваша сериализация JSON способна кодировать объект RWContentdata в строку. Это свойство соответствует разделу all в Learn . Если сериализатор работает правильно, результат encodeToStringдолжен быть таким же, как content— иначе тест провалится. Щелкните зеленую стрелку рядом с функцией слева, чтобы запустить тест, и выберите android (:testDebugUnitTest) . Вы увидите, что тест проходит.

Теперь, чтобы проверить правильность десериализации, добавьте следующий метод:

@Test fun testDecodePlatformAll() { val data = "{\"platform\":\"all\",\"url\":\"https://www.raywenderlich.com/feed.xml\",\"image\":\"https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png\"}" val decoded = Json.decodeFromString(RWContent.serializer(), data) val content = RWContent( platform = PLATFORM.ALL, url = "https://www.raywenderlich.com/feed.xml", image = "https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png" ) assertEquals(content, decoded) }

По сути делаешь наоборот. Начиная с ответа JSON, вам нужно будет вызвать kotlinx.serializationdecodeFromString , чтобы построить ваш объект RWContent , а затем вы сравните его с тем, который вы ожидаете — . Если содержимое совпадает, тест успешно пройден. Запустите тест и убедитесь, что он проходит.decoded``content

Тестирование пользовательской сериализации

Чтобы протестировать свой собственный RWSerializer , вам сначала нужно определить:

private val serializers = serializersModuleOf(PLATFORM::class, RWSerializer)

Это serializersсвойство содержит сериализатор , который вам понадобится для кодирования и декодирования ваших данных.

Начните с создания теста кодирования:

@Test fun testEncodeCustomPlatformAll() { val data = PLATFORM.ALL val encoded = Json.encodeToString(serializers.serializer(), data) val expectedString = "\"all\"" assertEquals(expectedString, encoded) }

Когда вы получаете ответ, тело представляет собой строку ответа в формате JSON:

{ "platform":"all", "url":"https://www.raywenderlich.com/feed.xml", "image":"https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png" }

Чтобы проверить, правильно ли работает RWSerializer в приведенном выше тесте, проверьте, соответствует ли результат ответу JSON "all"после кодирования PLATFORM.ALLсвойства в строку.

Если это так, assertEqualsфункция вернет true, иначе тест не пройден.

Теперь добавьте тест декодера:

@Test fun testDecodeCustomPlatformAll() { val data = PLATFORM.ALL val decoded = Json.decodeFromString<PLATFORM>(data.value) assertEquals(decoded, data) }

Здесь вы делаете все наоборот. Из строки «все», возвращенной из data.value, вы хотите получить соответствующее PLATFORMзначение перечисления. Для этого вы звоните decodeFromStringи подтверждаете, является ли возвращенный объект тем, который вы ожидаете.

Проблемы

Вот несколько задач, которые помогут вам попрактиковаться в том, что вы узнали в этой главе. Если вы застряли, взгляните на решения в материалах к этой главе.

Задача 1: загрузить RSS-канал

В настоящее время вы загружаете различные разделы из свойства RW_CONTENT внутри FeedPresenter.kt . В этом задании вы:

  • Создайте свойство RW_ALL_FEED , содержащее содержимое RSS-канала одного из URL-адресов канала из RW_CONTENT .
  • Прочтите это свойство и проанализируйте его содержимое в общем модуле, чтобы оно было доступно для использования всеми приложениями.
  • В androidApp и desktopApp откройте файл FeedViewModel.kt внутри ui/home и заполните элементы этими новыми данными.
  • В iOSApp откройте FeedClient внутри extensions и в функции fetchFeeds добавьте новые каналы.

Задача 2: добавьте тесты в свою реализацию

Теперь, когда вы внедрили эту новую функцию, вы добавите тесты, чтобы гарантировать правильность реализации. Не забудьте проверить сценарии, в которых некоторые атрибуты недоступны в файле JSON или их больше, чем доступно в RWEntry.kt .

Примечание . У вас должна быть возможность прочитать строку JSON, содержащую контент, который нужно сериализовать, и последующий доступ к объекту.

Ключевые моменты

  • Обмен данными между локальными и удаленными приложениями требует, чтобы передаваемое содержимое было сериализовано и десериализовано в зависимости от того, отправляется оно или получено соответственно.
  • kotlinx.serialization — это мультиплатформенная библиотека, поддерживающая сериализацию. Он позволяет сериализовать/десериализовать JSON , буферы протокола , CBOR , свойства , HOCON , YAML и Apache Avro .
  • Вы можете создавать собственные сериализаторы, реализовав сериализацию/десериализацию для пользовательского типа, а затем связав его с этим классом.
  • Вы можете использовать typealias с фактическим , чтобы автоматически связать объявление класса с существующим на уровне платформы.

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

Другие практические примеры использования Parcelize можно найти в статье.

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