Avatar ·

Android Room: как исправить ошибку "Cannot access database on the main thread" при простом SELECT запросе?

Всем привет! Я новичок в Android-разработке на Kotlin и использую Room для локальной базы данных. У меня есть простая сущность User и DAO с методом getAllUsers(), который возвращает список всех пользователей. Я вызываю этот метод из Activity, чтобы показать данные в RecyclerView. Но приложение вылетает с ошибкой:

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Вот мой код:

// Entity
@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    val name: String,
    val email: String
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<User>
}

// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

// Activity
class MainActivity : AppCompatActivity() {
    private lateinit var db: AppDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "my-database"
        ).build()

        // Вот здесь происходит краш
        val users = db.userDao().getAllUsers()
        Log.d("MainActivity", "Users: $users")
    }
}

Я понимаю, что проблема в том, что я вызываю запрос на главном потоке. Но как правильно это сделать? Нужно ли использовать AsyncTask или есть более современный способ? Подскажите, пожалуйста, как исправить эту ошибку и правильно выполнять запросы к Room в Kotlin.

<<>>

Почему возникает ошибка и как её исправить

Ошибка Cannot access database on the main thread возникает, потому что Room по умолчанию запрещает выполнение длительных операций (например, запросов к БД) на главном (UI) потоке. Это сделано для того, чтобы не блокировать интерфейс пользователя. Если бы Room разрешал такие запросы, приложение "зависало" бы на время выполнения запроса, особенно если данных много.

В современной Android-разработке с использованием Kotlin лучший способ решить эту проблему — использовать корутины (coroutines). Они позволяют выполнять асинхронные операции без сложностей, характерных для AsyncTask или Thread.

Вот пошаговое решение:

1. Добавьте зависимости coroutines в build.gradle

// В build.gradle (Module: app)
dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
}

2. Используйте ViewModel + LiveData или Flow для связи с UI

Лучшая практика — не вызывать запросы напрямую из Activity, а использовать ViewModel. Это отделяет логику от UI и предотвращает утечки памяти.

// UserViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

class UserViewModel(private val userDao: UserDao) : ViewModel() {

    // Room поддерживает Flow — это реактивный способ получать данные
    val allUsers: Flow<List<User>> = userDao.getAllUsers()

    // Альтернатива: если вам нужно выполнить запрос один раз
    fun fetchUsersOnce() {
        viewModelScope.launch {
            val users = userDao.getAllUsers() // suspend функция
            // Обработка результата (например, обновление LiveData)
        }
    }
}

Обратите внимание: метод getAllUsers() в DAO должен возвращать Flow<List<User>> или быть suspend. В вашем случае он уже suspend, но для реактивного обновления лучше использовать Flow:

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): Flow<List<User>>  // Room сам выполнит запрос в фоне
}

3. Подпишитесь на Flow в Activity/Fragment

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "my-database"
        ).build()

        val viewModelFactory = UserViewModelFactory(db.userDao())
        viewModel = ViewModelProvider(this, viewModelFactory).get(UserViewModel::class.java)

        // Подписка на Flow — автоматически выполняется в фоновом потоке
        lifecycleScope.launch {
            viewModel.allUsers.collect { users ->
                Log.d("MainActivity", "Users: $users")
                // Здесь обновляйте RecyclerView
            }
        }
    }
}

4. Если нужно выполнить запрос один раз (без Flow)

Используйте viewModelScope.launch внутри ViewModel:

class UserViewModel(private val userDao: UserDao) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users

    fun loadUsers() {
        viewModelScope.launch {
            val result = userDao.getAllUsers() // suspend функция
            _users.value = result
        }
    }
}

В Activity подпишитесь на LiveData:

viewModel.users.observe(this) { users ->
    Log.d("MainActivity", "Users: $users")
}

Ключевые моменты:

  • Никогда не вызывайте Room-запросы на главном потоке — используйте корутины, Flow или LiveData.
  • ViewModel — обязательный компонент для правильной архитектуры.
  • Flow — лучший выбор для реактивного получения данных (автоматическое обновление при изменениях в БД).
  • Если вам нужно временное решение без ViewModel (не рекомендуется), можно использовать lifecycleScope.launch прямо в Activity.

Теперь ваше приложение не будет вылетать, а запросы к БД будут выполняться асинхронно, не блокируя UI.

<<>>

Альтернативный подход: использование AsyncTask (устаревший, но рабочий)

Если вы по каким-то причинам не можете использовать корутины (например, проект на Java или очень старая версия Android), можно применить AsyncTask. Однако учтите, что этот класс объявлен устаревшим в Android 11, и его не рекомендуется использовать в новых проектах.

class GetUsersTask(private val userDao: UserDao, private val callback: (List<User>) -> Unit) : AsyncTask<Void, Void, List<User>>() {
    override fun doInBackground(vararg params: Void?): List<User> {
        return userDao.getAllUsers() // Выполняется в фоновом потоке
    }

    override fun onPostExecute(result: List<User>) {
        callback(result) // Возвращаем результат в UI-поток
    }
}

// Использование в Activity
GetUsersTask(db.userDao()) { users ->
    Log.d("MainActivity", "Users: $users")
}.execute()

Недостатки AsyncTask:

  • Сложно обрабатывать ошибки.
  • Легко допустить утечку памяти (если Activity уничтожена, а задача ещё выполняется).
  • Не поддерживает отмену и реактивное обновление.

Поэтому, если вы пишете на Kotlin, настоятельно рекомендую использовать корутины — это современный, безопасный и удобный способ.