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'

Источник