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, настоятельно рекомендую использовать корутины — это современный, безопасный и удобный способ.