Вступление: почему «простой SELECT» превращается в головную боль
Вы пишете своё первое приложение на Android с Room и Kotlin. Всё идёт отлично, пока не доходит до чтения данных из базы. Вы пишете что-то вроде:
val user = userDao.getUserById(42)
textView.text = user.name
Запускаете — и приложение падает с исключением:
java.lang.IllegalStateException: Cannot access database on the main thread
since it may potentially lock the UI for a long period of time.
Знакомо? Этот вопрос на Stack Overflow собрал тысячи просмотров и сотни комментариев. Ошибка пугает новичков, но на самом деле это не баг — это защита от дурака. Room (как и любая грамотная ORM) не даёт вам случайно заблокировать интерфейс пользователя.
В этой статье мы разберём почему это происходит и как правильно выполнять SELECT-запросы в Android Room на Kotlin. Вы узнаете три проверенных подхода: LiveData, Coroutines и RxJava. Поехали!
Коротко о главном: почему Room блокирует main thread?
Android — однопоточная система по умолчанию. Весь UI работает на main thread (главном потоке). Если вы начнёте читать базу данных на этом потоке, интерфейс замрёт, пока запрос не завершится. При больших объёмах данных это может занять секунды — пользователь увидит «висящее» приложение.
Room перестраховывается: он запрещает любые долгие операции (чтение, запись) на главном потоке. Исключение — если вы явно разрешили это через .allowMainThreadQueries() при сборке базы. Но делать так не стоит — это антипаттерн.
Вот как выглядит типичная проблема:
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User // suspend — уже намёк!
}
// Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ❌ Ошибка!
val user = database.userDao().getUserById(42)
}
}
Даже если вы сделали функцию suspend, но вызываете её без корутины — Room всё равно выбросит исключение. Исправляем!
Подход 1: LiveData — просто и реактивно
Самый простой способ — вернуть из DAO LiveData. Room сам управляет потоком: запрос выполняется в фоне, а результат приходит в UI.
Шаг 1. Меняем DAO
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Int): LiveData<User>
}
Шаг 2. Подписываемся в Activity/Fragment
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
database.userDao().getUserById(42).observe(this) { user ->
textView.text = user.name
}
}
}
Плюсы: минимум кода, автоматическая отписка при уничтожении Activity, реактивность — данные обновляются сами при изменении таблицы.
Минусы: LiveData привязана к жизненному циклу, нельзя легко комбинировать с другими источниками данных (сетью, SharedPreferences).
Подход 2: Coroutines — современный Kotlin-way
Если вы используете Kotlin Coroutines (а вы должны), это самый элегантный способ. Функция помечается как suspend, а вызов оборачивается в корутину с правильным Dispatchers.IO.
Шаг 1. DAO с suspend
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User
}
Шаг 2. Вызов из ViewModel (рекомендуется)
class MainViewModel(private val userDao: UserDao) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun loadUser(userId: Int) {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
userDao.getUserById(userId)
}
_user.value = result
}
}
}
Шаг 3. В Activity наблюдаем за LiveData
viewModel.user.observe(this) { user ->
textView.text = user.name
}
viewModel.loadUser(42)
Плюсы: полный контроль над потоками, легко комбинировать с сетевыми запросами, отличная интеграция с Jetpack.
Минусы: нужно помнить про Dispatchers.IO и не забывать про viewModelScope.
Подход 3: RxJava — для любителей реактивных стримов
Если в проекте уже используется RxJava/RxKotlin, Room поддерживает и его. Возвращаем Single или Flowable.
Шаг 1. DAO с Rx
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Int): Single<User>
}
Шаг 2. Подписка с переключением потоков
disposable = database.userDao().getUserById(42)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ user ->
textView.text = user.name
}, { error ->
Log.e("TAG", "Error: ${error.message}")
})
Не забудьте добавить в build.gradle:
implementation "androidx.room:room-rxjava2:2.6.1"
Плюсы: мощные операторы для трансформации данных, привычный синтаксис для Rx-разработчиков.
Минусы: избыточен для простых запросов, требует ручного управления подписками (не забыть dispose()).
Сравнительная таблица подходов
| Критерий | LiveData | Coroutines | RxJava |
|---|---|---|---|
| Сложность | Низкая | Средняя | Высокая |
| Реактивность | Да (автообновление) | Нет (ручной вызов) | Да (Flowable) |
| Управление потоками | Автоматически |
БлогиРекомендаций по книгамЧитайте также
Тестирование в Java: unit, integration, e2e — полное руководство
Функциональное программирование в Swift: основы и примеры
|