Data-классы в Kotlin
Если вы Android-инженер и задумались над переходом с Java на Kotlin, эта заметка вам пригодится.
Ещё до того, как в Google решили объявить поддержку Kotlin на официальном уровне, нередко можно было увидеть среди рекомендаций по переходу с Java на Kotlin начать процесс перехода именно с написания unit-тестов на этом языке. Действительно, unit-тесты — наименее агрессивный путь экспансии Котлин на кодовую базу, однако вы вряд ли вы испытаете удовольствие от «сладкого» и лаконичного синтаксиса Kotlin, подталкивающее к приёмам функционального программирования, ведь в большинстве случаев unit-тесты — это императивный стиль. Но вы можете попробовать параллельно делать вкрапления языка с написания POJO посредством data-классов. О них и поговорим.
Допустим, вам необходимо написать класс сущности для фильма Movie. Воспользовавшись дата-классами, вы обойдётесь всего одной строчкой кода:
data class Movie(val id: Long, val title: String, val director: String, val releaseDate: Date)
Что вы тут получаете помимо лаконичности: — переопределённые методы equals(), hashCode() и toString() под капотом; — Immutable-класс, который неявно наследуется от Any (в отличие от Object в Джаве) с immutable-полями и неявными публичными сеттерами и геттерами для каждого. Причём создание экземпляра такого класса выглядит так (обратите внимание, что отсутствует ключевое слово new):
Movie(42L, "Isle of Dogs", "Wes Anderson", Date())
— метод copy(), позволяющий клонировать экземпляр данного класса и полезный, если вы, к примеру, захотите создать на основе существующего объекта новый неизменяемый объект, но с отличающимися значениями одного либо нескольких полей (при условии, что они не являются private). Этот подход станет первым шагом навстречу функциональному стилю:
val clonedMovie = existingMovie.copy(id = 43L)
Примечание № 1: во время создания экземпляра такого класса на Java посредством copy() придётся определить значения для каждого из полей. Примечание № 2: этот метод недоступен для экземпляров обычных классов (не data); — ну и, наконец, мы получим поддержку значений по умолчанию, которой можно будет заменить применение Builder-паттерна.
data class Movie(val id: Long = 0L, val title: String = "", val director: String = "", val releaseDate: Date, val description: String? = null) ... val movie = Movie(releaseDate = Date(), title = "The Darjeeling Limited")
Однако учтите, что data-классы пока не могут наследоваться друг от друга. Но вам могут пригодиться неявно абстрактные sealed-классы. Допустим, тогда, когда нужно определить разные состояния загрузки данных либо экрана. И в любых других ситуациях, в которых уместны ограниченные иерархии классов.
Data-классы + Parcelable
Уже начиная с версии 1.1.4, отсутствует необходимость писать boilerplate-реализации методов parcelable для поддержки сериализации и десериализации ваших объектов, ведь за вас это делает аннотация @Parcelize.
@Parcelize data class Movie(val id: Long, val title: String, val director: String, val releaseDate: Date)
Просто не забудьте применить плагин Android Extensions:
apply plugin: 'kotlin-android-extensions'
А также надо будет определить значение experimental-флага как true.
android { ... androidExtensions { experimental = true } }
Data-классы и часто используемые библиотеки
Если вы любите использовать такую замечательную библиотеку, как Room Persistence Library, вы всё так же при подключённом kapt сможете писать data-классы для сущностей вашей БД, которые отлично работают с Room-аннотациями.
@Entity(tableName = "movies") data class Movie( @PrimaryKey @ColumnInfo(name = "id") val id: Long, @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "director") val director: String, @ColumnInfo(name = "date") val releaseDate: Long )
Нет проблем и с Nullable-свойствами.
Если же говорить о библиотеках для сериализации данных, в случае с одной из наиболее популярных в этой сфере GSON, необходимость в дополнительных конвертерах и плагинах отсутствует. Библиотека станет сериализовать ваши POJO с тем же успехом, как и в случае с Java-версией:
data class Movie( @SerializedName("id") val id: Long, @SerializedName("title") val title: String, @SerializedName("director") val director: String, @SerializedName("releaseDate") val releaseDate: Date )
Но, как и в случае с Room, не поддерживаются значения по умолчанию.
Также для совместимости Jackson c Kotlin надо добавить зависимость специального модуля.
Схожим образом обстоят дела и с Moshi, для совместимости с которым нужно добавить зависимость:
implementation 'com.squareup.moshi:moshi-kotlin:1.x.y'