Перейти к содержимому

Kotlin parcelize как использовать

  • автор:

О метке

kotlin-parcelize — Gradle-плагин для удобной работы с Parcelable в Android. Используйте эту метку для вопросов, связанных с использованием аннотации @Parcelize.

Для этой метки до сих пор нет описания.

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

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

(Обратите внимание: если у вас меньше 20000 баллов репутации, то перед публикацией ваши изменения в описании метки должны будут пройти проверку).

Передача массива при помощи Parcelable Kotlin

возникла следующая проблема: я пытаюсь передать массив при помощи Parcelable из одной activity в другую. Задаю дата класс и передаю следующим образом:

@Parcelize data class Gadget ( val gadgetValues: MutableList) :Parcelable private fun sendGadget ()

и принимаю результат в другой активности: val itemGadgets = intent.getParcelableExtra(«extra_Gadget») Но в переменную почему то передается null. В чем я ошибся?

Отслеживать

8,571 4 4 золотых знака 21 21 серебряный знак 36 36 бронзовых знаков

Урок 68. Немного о Parcel

Сам по себе Parcel мне никогда еще использовать не приходилось и не знаю, придется ли. Меня он заинтересовал, когда я начал разбираться с интерфейсом Parcelable. Этот интерфейс используется при передаче объектов через Intent и мне стало интересно, как создавать свои объекты с поддержкой такой передачи. В итоге я немного разобрался в Parcel и Parcelable, хотя понял далеко не все. Попробую теперь рассказать об этом.

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

Project name: P0681_Parcel
Build Target: Android 2.3.3
Application name: Parcel
Package name: ru.startandroid.develop.p0681parcel
Create Activity: MainActivity

В этом уроке экран нам не понадобится, main.xml оставляем без изменений. Работать будем с логом.

Кодим в MainActivity.java:

Методы readParcel и logReadInfo – пока пустые. Позже заполним.

Все сохраняем и запускаем приложение. Смотрим лог.

before writing: dataSize = 0
byte: dataSize = 4
int: dataSize = 8
long: dataSize = 16
float: dataSize = 20
double: dataSize = 28
String: dataSize = 52

Разбираем по порядку.

before writing: перед записью у нас размер данных равен 0. Записали byte: dataSize = 4 (для записи данных типа byte использовались 4 байта). Записали int: dataSize = 8 (для записи данных типа int использовались еще 4 байта в дополнение к ранее заполненным 4 байтам для byte). Записали long: dataSize = 16 (для записи long использовались еще 8 байтов в дополнение к ранее заполненным 8 байтам для byte и int). И т.д. В итоге видим, что dataSize показывает, сколько всего занято байт.

Обратите внимание, что типы int, long, float и double заняли столько байт, сколько они действительно занимают в Java – соответственно 4, 8, 4 и 8. byte – вместо одного байта почему-то занял целых 4. А String под каждый символ использует два байта, но пишет еще служебную информацию, поэтому получается больше.

Теперь попробуем прочесть то, что записали. Заполним пустые методы чтения:

Все сохраним, запустим приложение и смотрим лог.

Первые строки лога про запись нам уже знакомы. Нас интересуют строки чтения.

before reading: dataPosition = 52
byte = 1: dataPosition = 4
int = 2: dataPosition = 8
long = 3: dataPosition = 16
float = 4.0: dataPosition = 20
double = 5.0: dataPosition = 28
string = abcdefgh: dataPosition = 52

Перед тем, как мы установим позицию в 0 (before reading), видим, что она равна 52. Там она находится после записи. Каждая запись данных перемещает позицию на кол-во, равное размеру записываемых данных. Размер всех последовательно записанных данных у нас составил 52, и позиция соответственно переместилась в 52. Вы можете в качестве эксперимента выводить в лог позицию после каждой записи данных. Я же вывожу только для процедур чтения.

Итак, мы устанавливаем позицию в 0 и начинаем читать данные. Прочли значение byte, оно равно 1, как мы и записывали. Позиция сместилась на размер прочтенного значения, и теперь мы будем читать с позиции 4. Читаем int, оно равно 2, позиция сместилась и равна 8. И т.д.

Все значения, которые мы последовательно записывали, мы в том же порядке считали. Здесь надо понимать, что если вы записали int, а читать потом будете double, то результат получится не тот, что нужен. Т.к. int пишет 4 байта, а double считывает 8. Тем самым он залезет на следующий записанный тип и возьмет из него недостающие 4 байта. Получится каша. Поэтому тут надо быть аккуратным.

Вы всегда можете установить нужную вам позицию и считать хранимое значение. Главное – знать, какой тип там хранится. Например, у нас сейчас при записи double пишется с позиции 20. Поэтому мы можем перевести позицию в 20 и выполнить readDouble. Мы успешно получим записанный туда double, а позиция станет равна 28.

Если вы хотите глянуть содержимое Parcel можно использовать его метод marshall(), он вернет массив записанных в Parcel байтов.

Вот такой краткий экскурс. Эти знания понадобятся для понимания следующего урока.

На следующем уроке:

— добавляем своему объекту поддержку Parcelable
— передаем объект с помощью Intent

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

Пишем плагин Parcelize для компилятора Kotlin под iOS

В этой статье описан мой опыт по написанию плагина для компилятора Kotlin. Моей главной целью было создание плагина под iOS (Kotlin/Native), аналогичного kotlin-parcelize под Android. Дело в том, что в iOS, как и в Android, приложения тоже могут быть убиты системой, а значит, может возникнуть необходимость сохранять стек навигации и другие данные. В результате работы над этой задачей получился kotlin-parcelize-darwin. Подробности о его создании и применении — под катом.

Parcelize в Android

Хотя в статье будет описана разработка под iOS, давайте вспомним, что собой представляют интерфейс Parcelable и плагин kotlin-parcelize под Android. Интерфейс Parcelable позволяет нам сериализовать в Parcel реализующий класс, чтобы он был представлен в виде массива байтов. Также он позволяет десериализовать класс из Parcel для восстановления всех данных. Эта возможность широко используется для записи и восстановления состояний экрана, например когда приостановленное приложение сначала убивается системой из-за нехватки памяти, а затем снова активируется.

Реализовать интерфейс Parcelable не сложно. Нужно написать два основных метода:

  • writeToParcel(Parcel, …) — пишет данные в Parcel,
  • createFromParcel(Parcel) — читает из Parcel.

Необходимо записывать информацию поле за полем, а затем считывать в том же порядке. Звучит довольно просто, но писать шаблонный код надоедает. Кроме того, можно наделать ошибок, так что в идеале нужно писать ещё и тесты для классов Parcelable .

К счастью, для компилятора Kotlin есть плагин kotlin-parcelize . Если его включить, то достаточно лишь пометить класс Parcelable с помощью аннотации @Parcelize — и плагин автоматически сгенерирует реализацию. Это избавляет нас от написания соответствующего шаблонного кода и гарантирует корректность реализации на этапе компилирования.

Применение Parcelize в iOS

Поскольку iOS-приложения тоже имеют вышеупомянутые особенности, есть аналогичные способы сохранения состояния приложения. Один из них заключается в использовании протокола NSCoding, который очень похож на интерфейс Parcelable в Android. Классы тоже должны реализовать два метода:

  • encode(with: NSCoder) — кодирует объект в NSCoder,
  • init?(coder: NSCoder) — декодирует объект из NSCoder .

Kotlin Native под iOS

Kotlin не ограничен Android — его можно использовать для написания Kotlin Native-фреймворков под iOS и даже мультиплатформенного кода. А поскольку iOS-приложения тоже могут быть убиты системой, то с ними возникают те же проблемы. Kotlin Native под iOS предлагает двунаправленную совместимость с Objective-C, то есть мы можем использовать NSCoding и NSCoder .

Очень простой класс данных может выглядеть так:

Добавим реализацию протокола NSCoding :

Выглядит просто. Попробуем скомпилировать:

Попробуем сделать так, чтобы касс User расширял класс NSObject :

И опять не компилируется!

Интересно. Похоже, компилятор пытается переопределить и сгенерировать метод toString , но для классов, наследующихся от NSObject , нам нужно переопределить метод description . Кроме того, нам, вероятно, вообще не стоит использовать наследование, потому что это может помешать пользователям расширять их собственные классы Kotlin (из-за невозможности множественного наследования).

Parcelable для iOS

Нам нужно другое решение, без использования наследования. Определим интерфейс Parcelable :

Всё просто. Классы Parcelable будут иметь только метод coding , который возвращает экземпляр NSCodingProtocol . Остальное будет обработано реализацией протокола.

Теперь давайте изменим класс User таким образом, чтобы он реализовал интерфейс Parcelable :

Мы создали вложенный класс CodingImpl , который реализует протокол NSCoding . Метод encodeWithCoder остался неизменным, а вот с initWithCoder ситуация чуть сложнее. Он должен возвращать экземпляр протокола NSCoding , но класс User теперь им не является. Нам нужно какое-то обходное решение, промежуточный класс:

Класс DecodedValue реализует протокол NSCoding и хранит некоторый объект value . Все методы могут быть пустыми, потому что этот класс не будет ни кодироваться, ни декодироваться. Теперь мы можем использовать его в методе initWithCoder класса User :

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

Давайте напишем тест, чтобы проверить, всё ли работает правильно.

  1. Создаём экземпляр класса User с какими-нибудь данными.
  2. Кодируем его с помощью NSKeyedArchiver , в качестве результата получаем NSData .
  3. Декодируем NSData с помощью NSKeyedUnarchiver .
  4. Убеждаемся, что декодированный объект аналогичен исходному.

Пишем плагин для компилятора

Мы определили интерфейс Parcelable под iOS, попробовали его в работе с помощью класса User и протестировали код. Теперь можно автоматизировать реализацию Parcelable , чтобы код генерировался автоматически, как при использовании kotlin-parcelize под Android.

Мы не можем использовать Kotlin Symbol Processing (KSP), потому что это не позволит нам менять существующие классы, а только генерировать новые. Так что единственным решением будет написать плагин для компилятора Kotlin. Но это не так просто, в основном потому, что документации до сих пор нет, API работает нестабильно и т. д. Если вы всё же соберётесь писать плагин для компилятора, то рекомендую обратиться к этим источникам:

  • «Волшебство расширений для компилятора» — выступление Андрея Шикова,
  • Writing Your Second Kotlin Compiler Plugin” — статья Брайана Нормана.

Плагин работает так же, как kotlin-parcelize . Классы должны реализовывать интерфейс Parcelable и быть помечены с помощью аннотации @Parcelize . При компилировании плагин генерирует реализации Parcelable . Классы Parcelable выглядят так:

Название плагина

Плагин называется kotlin-parcelize-darwin . Часть “-darwin” означает, что плагин должен работать со всеми платформами Darwin (Apple), но сейчас нас интересует только iOS.

Gradle-модули

  1. kotlin-parcelize-darwin — первый модуль, который нам понадобится. Он содержит плагин для Gradle, который регистрирует плагин для компилятора, и ссылается на два артефакта: один — для плагина компилятора Kotlin/Native, второй — для плагина компилятора под все другие платформы.
  2. kotlin-arcelize-darwin-compiler — модуль плагина для компилятора Kotlin/Native.
  3. kotlin-parcelize-darwin-compiler-j — модуль плагина для ненативного компилятора. Он необходим, потому что является обязательным и на него ссылается Gradle-плагин. Хотя на самом деле этот модуль пустой, поскольку нам ничего не нужно из ненативного варианта.
  4. kotlin-parcelize-darwin-runtime — содержит зависимости времени выполнения (runtime) для компиляторного плагина. Например, здесь находятся интерфейс Parcelable и аннотация @Parcelize .
  5. tests — содержит тесты для компиляторного плагина, добавляет в плагин модули в виде included builds.

Процесс установки плагина

В корневом файле build.gradle :

В файле build.gradle проекта:

Реализация

Генерирование кода в Parcelable состоит из двух основных этапов. Нам нужно:

  1. Сделать код компилируемым с помощью добавления синтетических заглушек для отсутствующих методов fun coding(): NSCodingProtocol из интерфейса Parcelable .
  2. Сгенерировать реализации для заглушек, добавленных на предыдущем этапе.

Добавление заглушек

Это делается с помощью класса ParcelizeResolveExtension, который реализует интерфейс SyntheticResolveExtension . Всё очень просто: класс реализует методы getSyntheticFunctionNames и generateSyntheticMethods , которые вызываются при компилировании для каждого класса.

Как видите, сначала нужно проверить, можно ли применять логику Parcelize к текущему классу. Для этого используется функция isValidForParcelize :

Мы обрабатываем только те классы, у которых есть аннотация @Parcelize и которые реализуют интерфейс Parcelable .

Генерирование реализаций заглушек

Как вы могли догадаться, этот этап создания плагина значительно сложнее. За него отвечает класс ParcelizeGenerationExtension, который реализует интерфейс IrGenerationExtension . Он содержит всего один метод:

Нам необходимо пройтись по всем классам, содержащимся в предоставленном нам IrModuleFragment . Для этого используется класс ParcelizeClassLoweringPass, который наследует ClassLoweringPass . Класс ParcelizeClassLoweringPass переопределяет только один метод:

Проходить по классам не сложно:

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

Итак, сначала нам снова нужно проверить, можно ли применять логику Parcelize к текущему классу ( irClass ):

Затем добавим в irClass вложенный класс CodingImpl , определим его супертипы ( NSObject и NSCoding ) и пометим аннотацией @ExportObjCClass (чтобы класс был доступен при поиске во время выполнения):

Теперь добавим в класс CodingImpl первичный конструктор. У него должен быть только один аргумент — data: TheClass , поэтому нам также надо сгенерировать поле (field) data , свойство (property) и метод считывания (getter).

Вот что у нас получается:

Добавим реализацию протокола NSCoding :

Теперь сгенерированный класс выглядит так:

Наконец нам нужно сгенерировать тело метода coding() , просто создав экземпляр класса CodingImpl :

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

Плагин задействуется, когда мы пишем на Kotlin классы Parcelable . Обычно его используют для сохранения состояний экрана. Плагин позволяет восстанавливать исходное состояние приложения после того, как оно было убито iOS. Другой сценарий использования — сохранение стека навигации в тех случаях, когда она реализована на Kotlin.

Вот обобщённый пример использования Parcelable в Kotlin, который демонстрирует, как можно сохранить и восстановить данные:

А вот пример того, как мы можем кодировать и декодировать классы Parcelable в iOS-приложении:

Parcelize в Kotlin Multiplatform

Теперь у нас есть два плагина: kotlin-parcelize для Android и kotlin-parcelize-darwin — для iOS. И мы можем применить их оба и использовать @Parcelize в общем коде!

Файл build.gradle нашего общего модуля будет выглядеть так:

Теперь у нас в наборах androidMain и iosMain есть доступ к интерфейсам Parcelable и аннотациям @Parcelize . Чтобы использовать их в commonMain , нам нужно вручную определить их с помощью expect/actual .

Напишем в commonMain :

Во всех остальных наборах:

Теперь мы можем использовать Parcelize как обычно в commonMain . При сборке под Android код будет обработан плагином kotlin-parcelize , а при сборке под iOS — плагином kotlin-parcelize-darwin . В случае с другими платформами ничего не будет сделано, потому что интерфейс Parcelable будет пуст, а аннотация будет отсутствовать.

Заключение

Мы рассмотрели компиляторный плагин kotlin-parcelize-darwin . Исследовали его структуру и принцип работы, узнали, как его можно применять в Kotlin Native, как подружить его с Android-плагином kotlin-parcelize в Kotlin Multiplatform, а также как использовать Parcelable на стороне iOS.

Исходный код лежит на GitHub:

arkivanov/kotlin-parcelize-darwin

Плагин ещё не опубликован, но вы уже можете с ним экспериментировать, опубликовав в локальном Maven-репозитории или с помощью Gradle Composite builds.

В репозитории также есть очень простой пример проекта, в котором есть общий модуль, а также Android- и iOS-приложения. Спасибо, что дочитали, и не забудьте подписаться на меня в Twitter!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *