Data-классы и sealed-классы

data class генерирует equals, hashCode, toString и copy автоматически, а sealed class задаёт закрытую иерархию типов — идеальный инструмент для моделирования состояний экрана.
Суть: data-классы избавляют от шаблонного кода для моделей данных, а sealed-классы делают ветвление по состояниям исчерпывающим и безопасным.

Очень часто класс нужен только для того, чтобы хранить набор полей: пользователь, товар, сообщение. Для этого есть data class. Достаточно объявить его с параметрами — и компилятор сам сгенерирует equals/hashCode (сравнение по значениям полей), toString (читаемый вывод) и copy (копия с изменением части полей).

data class User(val name: String, val age: Int)

val u1 = User("Аня", 19)
val u2 = User("Аня", 19)
println(u1 == u2)            // true: сравнение по значениям
println(u1)                  // User(name=Аня, age=19)

val older = u1.copy(age = 20) // копия с новым возрастом
println(older)               // User(name=Аня, age=20)

Метод copy особенно важен в Android: состояние экрана обычно описывают неизменяемым data-классом, а при изменении создают копию с новыми значениями. Это основа предсказуемого обновления UI в Compose.

Деструктуризация

Data-классы поддерживают разбор на отдельные переменные одной строкой.

val (name, age) = User("Иван", 21)
println(name)  // Иван
println(age)   // 21

Sealed-классы

sealed class описывает закрытую иерархию: все наследники известны на этапе компиляции и обычно лежат в том же файле. Это идеально для моделирования ограниченного набора состояний — например, загрузка данных может быть в состоянии «идёт загрузка», «успех с данными» или «ошибка».

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<String>) : UiState()
    data class Error(val message: String) : UiState()
}

fun render(state: UiState): String = when (state) {
    is UiState.Loading -> "Загрузка..."
    is UiState.Success -> "Данные: ${state.data}"
    is UiState.Error -> "Ошибка: ${state.message}"
    // else не нужен: when исчерпывающий
}

Как работает под капотом

Поскольку компилятор знает полный список наследников sealed-класса, он проверяет when на исчерпывающность. Если вы обработали не все варианты, код не скомпилируется (для when-выражения) или вы получите предупреждение. Это бесценно: добавив новое состояние, вы сразу увидите все места, которые нужно обновить. Внутри smart cast после is UiState.Success даёт прямой доступ к полю data без явного приведения.

  sealed class UiState
        |
   +----+--------+--------+
   v             v        v
  Loading     Success    Error
  (object)   (data:...) (msg:...)

  when(state) обязан покрыть все три ветки

Частые ошибки

Использовать обычный класс там, где нужен data. Без data сравнение идёт по ссылке, а не по значениям, и toString выводит мусор.

Менять поля копии вместо создания новой. Состояние держите неизменяемым и применяйте copy — это основа корректной работы Compose.

Добавлять else в when по sealed без нужды. Лишний else прячет ситуацию, когда вы забыли обработать новое состояние.

Best practices

  • Для моделей данных используйте data class с неизменяемыми val-полями.
  • Обновляйте состояние через copy, а не мутацией полей.
  • Моделируйте состояния экрана через sealed class и обрабатывайте их исчерпывающим when.
  • Избегайте else в when по sealed, чтобы компилятор ловил необработанные случаи.

Идею sealed-состояний и исчерпывающего ветвления удобно прорепетировать на Python-редьюсере. Запустите врезку.

# Аналог sealed UiState + when из Kotlin
def render(state):
    kind = state['kind']
    if kind == 'loading': return 'Загрузка...'
    if kind == 'success': return 'Данные: ' + str(state['data'])
    if kind == 'error':   return 'Ошибка: ' + state['message']
    raise ValueError('Неизвестное состояние: ' + kind)

states = [
    {'kind': 'loading'},
    {'kind': 'success', 'data': ['a', 'b']},
    {'kind': 'error', 'message': 'нет сети'},
]
for s in states:
    print(render(s))

Попробуй сам ▶ — добавьте новое состояние (например, пустой список) и обработайте его. В Kotlin компилятор сам напомнил бы, что новое состояние нужно обработать в when.

Итог: data-классы убирают шаблонный код моделей и дают незаменимый copy, а sealed-классы делают состояния экрана типобезопасными. Эта связка — фундамент архитектуры современного Android-приложения на Compose.

Проверьте себя
1. Какой метод data-класса чаще всего используют для обновления состояния экрана в Compose?
Aequals — для сравнения
BtoString — для вывода
Ccopy — создаёт копию объекта с изменением части полей
DhashCode — для хеширования
2. В чём главное преимущество sealed class при использовании во when?
AОн работает быстрее обычного класса
BКомпилятор знает все наследники и проверяет when на исчерпывающность
CОн автоматически создаёт базу данных
DЕго наследники могут лежать в любом модуле проекта