Introdução: por que um "SELECT simples" se torna uma dor de cabeça
\n\nVocê está escrevendo seu primeiro aplicativo Android com Room e Kotlin. Tudo vai bem, até chegar a hora de ler dados do banco. Você escreve algo como:
\n\nval user = userDao.getUserById(42)\ntextView.text = user.name\n\nVocê executa — e o aplicativo trava com a exceção:
\n\njava.lang.IllegalStateException: Cannot access database on the main thread\nsince it may potentially lock the UI for a long period of time.\n\nFamiliar? Esta pergunta no Stack Overflow acumulou milhares de visualizações e centenas de comentários. O erro assusta iniciantes, mas na verdade não é um bug — é uma proteção contra o usuário descuidado. O Room (como qualquer ORM competente) não permite que você bloqueie acidentalmente a interface do usuário.
\n\nNeste artigo, vamos analisar por que isso acontece e como executar corretamente consultas SELECT no Android Room com Kotlin. Você conhecerá três abordagens comprovadas: LiveData, Coroutines e RxJava. Vamos lá!
\n\nResumindo: por que o Room bloqueia a main thread?
\n\nO Android é um sistema single-thread por padrão. Toda a interface do usuário (UI) roda na main thread (thread principal). Se você começar a ler o banco de dados nesta thread, a interface congelará até que a consulta termine. Com grandes volumes de dados, isso pode levar segundos — o usuário verá um aplicativo "travado".
\n\nO Room é cauteloso: ele proíbe qualquer operação longa (leitura, gravação) na thread principal. A exceção é se você permitir explicitamente através de .allowMainThreadQueries() ao construir o banco. Mas não faça isso — é um antipadrão.
Veja como é o problema típico:
\n\n// DAO\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User // suspend — já é uma dica!\n}\n\n// Activity\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n // ❌ Erro!\n val user = database.userDao().getUserById(42)\n }\n}\n\nMesmo se você tornar a função suspend, mas chamá-la sem uma coroutine — o Room ainda lançará a exceção. Vamos corrigir!
Abordagem 1: LiveData — simples e reativo
\n\nA maneira mais simples é retornar LiveData do DAO. O próprio Room gerencia a thread: a consulta é executada em segundo plano e o resultado chega na UI.
\n\nPasso 1. Alterar o DAO
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n fun getUserById(userId: Int): LiveData<User>\n}\n\nPasso 2. Inscrever-se na Activity/Fragment
\n\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n database.userDao().getUserById(42).observe(this) { user ->\n textView.text = user.name\n }\n }\n}\n\nVantagens: mínimo de código, cancelamento automático da inscrição quando a Activity é destruída, reatividade — os dados são atualizados automaticamente quando a tabela é alterada.
\n\nDesvantagens: LiveData está vinculada ao ciclo de vida, não é fácil combinar com outras fontes de dados (rede, SharedPreferences).
\n\nAbordagem 2: Coroutines — o caminho moderno do Kotlin
\n\nSe você usa Kotlin Coroutines (e deveria), esta é a maneira mais elegante. A função é marcada como suspend e a chamada é envolvida em uma coroutine com o Dispatchers.IO correto.
Passo 1. DAO com suspend
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User\n}\n\nPasso 2. Chamada a partir da ViewModel (recomendado)
\n\nclass MainVi