При описании работы UI я использую концепцию «события», которое передаётся в логику UI слоя для дальнейшей обработки. Это позволяет мне не делать множество методов и с помощью Kotlin sealed классов описать все возможные события, а также форсировать их обработку через код
// Описываются события который могут происходить в UI
// чтобы потом отправить в код логики обработки
sealed interface UiEvent {
/** Выбран элемента из списка */
data class UserProfileClicked(val userId: String) : UiEvent
/** Нажатие на кнопку подтверждения действия */
data object ConfirmClicked : UiEvent
}
class MyViewModel: ViewModel() {
fun onUiEvent(event: UiEvent): Unit = when(event) {
is UserProfileClicked -> // Обработка события из UI
is ConfirmClicked -> // Обработка события из UI
}
}
В статье я не буду обсуждать того, как такой подход тратит память на лишние выделения памяти и работу сборщика мусора. Суть текста в другом
Но этот код имеет одну неточность, которую мне рекомендует IDEA/Android Studio, а многие программисты её принимают на веру как априоре правильную - заменить класс без параметров на data object
Неосознанное использование синглтонов в Kotlin
object
представляет собой singletone т.е. существует только один экземпляр этого класса в рамках одного процесса и другого быть не может (опускаем случае создание через рефлексию и другие возможности платформ). data object
, который появился в Kotlin 1.9, сгенерирует читаемый toString()
а также hashcode()
и equals()
методы.
В моём коде я описываю логику отправки события, которое каждое является уникальным и точно не подходит под описание "один экземпляр на всё приложение". Будь у моего класса хотя бы один параметр в primary конструкторе, то такого предупреждения от IDE с "полезным" советом я не получил. Вам как разработчику надо всё также помнить что вы переносите объект в код. Если нет четкой причины сделать класс data/value/object, то не делайте этого.
Мой же код описания события теперь примет следующий вид:
sealed interface UiEvent {
data class UserProfileClicked(val userId: String) : UiEvent
class ConfirmClicked() : UiEvent
}
Также аргументом в отказ от object я привожу, что зачастую классы события со временем могут меняться и дополняться параметрами. При изменении object на класс у вас будет теряться совместимость вызова, так как уже потребуется вызывать конструктор. Выходы есть и из такой ситуации, но всё таки лучше сразу стараться описать всё гибко сразу.
Использование data классов
Добавление data
для классов-моделей стало уже неотъемлимой частью написания такого кода.
data class приведет к генерации:
Перегрузки equals() и hashcode()
Перегрузки toString()
Созданию функции copy()
Созданию функций-операторов componentN() для использование spread оператора
Зачастую нам нужны именно только перегрузки стандартных методов, по аналогии с теми что появится в результате добавления data
к классу, а вот всё остальное даже не пригодится, может и вовсе вы не хотите таких возможностей класса.
R8 или ProGuard помогут вам оптимизировать код и удалять неиспользуемые API классов, так что переживать за лишний код точно не стоит. Обязательно используете оптимизаторы кода на продакшен сборках!
В случае UI событий вполне нормальным может являться. что любые экземпляры класса, представляющего событие, equals() всегда должен возвращать false т.к. они созданы в результате действия пользователя. Тогда может стоит оставить стандартную реализацию equals() и hashcode()? Да, вполне рабочий подход
sealed interface UiEvent {
class UserProfileClicked(val userId: String) : UiEvent
class ConfirmClicked() : UiEvent
}
Проблема его что сломаются все проверки в тестах, что ваши пришедшее событие совпадает с ожидаемым при использование assertEquals(expectedEvent, incomingEvent)
.
Нет тестов - нет проблем ?
Вариантом может быть добавление в свойства уникального идентификатора события: момент времени создания, уникальный ID и др. Это позволит нам добиться желаемой уникальности в событиях
sealed interface UiEvent {
/**
* Уникальный идентификатор события
*/
val id: Uuid
data class UserProfileClicked(
val userId: UserId,
override val id: Uuid = Uuid.random(),
) : UiEvent
data class ConfirmClicked(
override val id: Uuid = Uuid.random(),
) : UiEvent
}
Kotlin предоставил много возможностей и позволил убрать написание однотипного кода, но это стало тем что разработчики стали забывать, что не весь синтаксический сахар позволит реализовать задуманное. В Java же сложность написания чего-то заставляла нас делать выбор осознанным. Не используйте бездумно всё, что вам предлагает язык программирования и IDE, реализуйте логику работы объектов вашей программы соотвественно их поведению!
Что-то из моего примера вам покажется оверинженирингом, ведь с проблемой вы не стоклнулись. Буду рад в комментариях услышать ваше мнение касательно моих предложений и другого синтаксического сахара Kotlin, который может идти во вред.
xlzpm
Прочел статью. Понял, что все и в правду вошло в привычку, не представляю, как будут смотреть ребята pull request, когда я вместо object для перехода на другой экран, пропишу class, посмотрят как на чокнутого, но опять data object и предполагает под себя единый класс, который передает какой то state в effect