Avatar ·

Ошибка "Cannot access database on the main thread" в Android Room на Kotlin

Привет! Я новичок в Android-разработке на Kotlin и использую Room для локального хранения данных. Написал простой запрос SELECT, но приложение вылетает с ошибкой: "Cannot access database on the main thread". Вот мой код: ```kotlin // DAO @Dao interface UserDao { @Query("SELECT * FROM users WHERE id = :userId") fun getUser(userId: Int): User } // 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() val user = db.userDao().getUser(1) // Здесь ошибка! textView.text = user.name } } ``` Я понимаю, что Room запрещает работу с базой на главном потоке, но как тогда правильно получать данные? Пробовал использовать async/await, но не разобрался. Подскажите, пожалуйста, как исправить этот код, чтобы запрос выполнялся в фоне, а результат отображался в UI. Использую Kotlin, Android Studio 3.0, API 26+. <<>>

Решение проблемы с помощью корутин 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.