Ошибка "Cannot find setter for field" в Room с Kotlin: причины и решение
Почему возникает ошибка?
Проблема в том, что Room для работы с сущностями использует рефлексию и требует наличия сеттеров (setter methods) для всех полей, которые будут загружаться из базы данных. В Kotlin data class с val генерирует только геттеры, а сеттеры создаются только для var. Когда Room пытается восстановить объект из курсора (например, при выполнении SELECT), он ищет метод setName(...), но не находит его — отсюда и ошибка.
Решение: используйте var или конструктор с полями по умолчанию
Есть два основных подхода:
1. Использовать var (самый простой способ)
Room может работать с var-полями. Это безопасно, если вы не экспортируете объекты за пределы базы данных без копирования:
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
var name: String, // var вместо val
var age: Int // var вместо val
)
2. Использовать val с конструктором по умолчанию (рекомендуемый способ)
Room может работать с неизменяемыми полями, если предоставить пустой конструктор или конструктор со значениями по умолчанию. Компилятор Kotlin + Room обработает это через рефлексию:
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String = "", // значение по умолчанию
val age: Int = 0 // значение по умолчанию
)
Room создаст экземпляр класса через пустой конструктор (который Kotlin генерирует автоматически, если все поля имеют значения по умолчанию), а затем заполнит поля через рефлексию, игнорируя сеттеры. Это работает, потому что Room напрямую пишет в backing field объекта.
Важные нюансы
- Для @Ignore полей — если поле помечено
@Ignore, Room не требует сеттера, так как не загружает его из БД. - Вложенные объекты — если используете
@Embedded, то вложенный объект тоже должен иметь сеттеры или конструктор по умолчанию. - TypeConverters — если вы используете конвертер типов, то поле должно быть var, иначе Room не сможет присвоить конвертированное значение.
Полный рабочий пример
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String = "",
val age: Int = 0
)
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUser(id: Int): User?
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Теперь ваш код будет работать без ошибок, а поля останутся неизменяемыми (immutable) после создания объекта. Room сам позаботится о заполнении полей через рефлексию.
<<Альтернативный подход: использование @JvmField
Если вы хотите сохранить val и не задавать значения по умолчанию, можно использовать аннотацию @JvmField. Она заставляет Kotlin-компилятор генерировать поле без приватного модификатора, что позволяет Room обращаться к нему напрямую:
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
@JvmField val name: String,
@JvmField val age: Int
)
Плюсы: поля остаются val (неизменяемыми), не нужно задавать значения по умолчанию.
Минусы:
@JvmFieldделает поле публичным (public), что может нарушить инкапсуляцию.- Не работает с некоторыми плагинами, которые анализируют байт-код (например, ProGuard/R8 может удалить поле).
- Может вызвать проблемы с сериализацией (Gson, Moshi и т.д.), если они ожидают стандартные Kotlin-свойства.
Я рекомендую использовать первый вариант (значения по умолчанию) — он более идиоматичен для Kotlin и не имеет побочных эффектов. Но если вам критично избежать значений по умолчанию, @JvmField — рабочий вариант.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги