Ошибка "Cannot access database on the main thread" в Android Room на Kotlin
Решение проблемы с помощью корутин Kotlin
Ошибка "Cannot access database on the main thread" возникает, потому что Room по умолчанию блокирует запросы на главном потоке (UI thread). Это сделано для предотвращения зависания интерфейса. Лучший способ решить проблему в современном Kotlin — использовать корутины (coroutines).
Вот пошаговое исправление вашего кода:
1. Обновите DAO, добавив suspend-функцию
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUser(userId: Int): User // Добавьте ключевое слово suspend
}
2. В Activity запустите корутину
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"my-db"
).build()
// Запускаем корутину в lifecycleScope (требуется API 26+)
lifecycleScope.launch(Dispatchers.IO) {
val user = db.userDao().getUser(1) // Выполняется в фоновом потоке
withContext(Dispatchers.Main) {
textView.text = user.name // Возвращаемся на главный поток для UI
}
}
}
}
Объяснение решения
- suspend-функция — это функция, которая может приостанавливать своё выполнение без блокировки потока. Room автоматически переключает такие запросы в фоновый поток.
- Dispatchers.IO — это пул потоков, оптимизированный для операций ввода-вывода (база данных, файлы, сеть).
- withContext(Dispatchers.Main) — переключает выполнение обратно на главный поток, где можно безопасно обновлять UI.
- lifecycleScope — встроенная корутина, которая автоматически отменяется при уничтожении Activity, предотвращая утечки памяти.
Альтернатива: использование LiveData
Если вы предпочитаете реактивный подход, можно вернуть LiveData из DAO:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
fun getUser(userId: Int): LiveData<User> // LiveData, не suspend
}
// В Activity:
db.userDao().getUser(1).observe(this) { user ->
textView.text = user.name // Автоматически на главном потоке
}
LiveData автоматически выполняет запрос в фоне и доставляет результат на главный поток. Это особенно удобно для данных, которые могут изменяться.
Важные моменты
- Добавьте зависимость в build.gradle:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" - Для Room версии 2.1+ корутины поддерживаются из коробки.
- Не используйте
allowMainThreadQueries()— это опасная практика, которая может "повесить" приложение при больших данных.
Использование RxJava для асинхронных запросов
Если в вашем проекте уже используется RxJava, это тоже отличный вариант. Room поддерживает возврат Observable, Flowable или Single из DAO.
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
fun getUser(userId: Int): Single<User> // Single для одного результата
}
// В Activity:
val disposable = db.userDao().getUser(1)
.subscribeOn(Schedulers.io()) // Фоновый поток
.observeOn(AndroidSchedulers.mainThread()) // Главный поток
.subscribe({ user ->
textView.text = user.name
}, { error ->
Log.e("RoomError", "Ошибка: ${error.message}")
})
// Не забудьте добавить disposable в CompositeDisposable для очистки
Этот подход особенно полезен, если вы уже работаете с RxJava в проекте. Однако для новых проектов я рекомендую корутины — они более лаконичны и легче в изучении. RxJava добавляет накладные расходы на управление подписками.
Обратите внимание: для Single не забудьте добавить зависимость androidx.room:room-rxjava2 или room-rxjava3 в build.gradle.