Einleitung: Warum ein "einfacher SELECT" zum Kopfzerbrechen wird
\n\nSie schreiben Ihre erste Android-App mit Room und Kotlin. Alles läuft hervorragend, bis es ans Auslesen von Daten aus der Datenbank geht. Sie schreiben so etwas wie:
\n\nval user = userDao.getUserById(42)\ntextView.text = user.name\n\nSie starten die App – und sie stürzt mit folgender Ausnahme ab:
\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\nKommt Ihnen das bekannt vor? Diese Frage auf Stack Overflow hat tausende Aufrufe und hunderte Kommentare gesammelt. Der Fehler erschreckt Anfänger, aber eigentlich ist es kein Bug – es ist ein Schutz vor Dummheit. Room (wie jedes gute ORM) verhindert, dass Sie versehentlich die Benutzeroberfläche blockieren.
\n\nIn diesem Artikel erklären wir warum das passiert und wie man SELECT-Abfragen in Android Room mit Kotlin korrekt ausführt. Sie lernen drei bewährte Ansätze kennen: LiveData, Coroutines und RxJava. Los geht's!
\n\nKurz und knapp: Warum blockiert Room den Hauptthread?
\n\nAndroid ist standardmäßig ein Single-Thread-System. Die gesamte UI läuft auf dem main thread (Hauptthread). Wenn Sie auf diesem Thread aus der Datenbank lesen, friert die Oberfläche ein, bis die Abfrage abgeschlossen ist. Bei großen Datenmengen kann das Sekunden dauern – der Benutzer sieht eine "hängende" App.
\n\nRoom geht auf Nummer sicher: Es verbietet alle langen Operationen (Lesen, Schreiben) auf dem Hauptthread. Die Ausnahme ist, wenn Sie dies explizit über .allowMainThreadQueries() beim Erstellen der Datenbank erlauben. Das sollten Sie aber nicht tun – es ist ein Anti-Pattern.
So sieht das typische Problem aus:
\n\n// DAO\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User // suspend – schon ein Hinweis!\n}\n\n// Activity\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n // ❌ Fehler!\n val user = database.userDao().getUserById(42)\n }\n}\n\nSelbst wenn Sie die Funktion als suspend deklariert haben, aber ohne Coroutine aufrufen – Room wirft trotzdem eine Ausnahme. Beheben wir das!
Ansatz 1: LiveData – einfach und reaktiv
\n\nDer einfachste Weg ist, aus dem DAO LiveData zurückzugeben. Room verwaltet den Thread selbst: Die Abfrage wird im Hintergrund ausgeführt, das Ergebnis kommt in der UI an.
\n\nSchritt 1: DAO ändern
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n fun getUserById(userId: Int): LiveData<User>\n}\n\nSchritt 2: In Activity/Fragment abonnieren
\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\nVorteile: Minimaler Code, automatisches Abmelden bei Zerstörung der Activity, Reaktivität – Daten aktualisieren sich automatisch bei Änderungen in der Tabelle.
\n\nNachteile: LiveData ist an den Lebenszyklus gebunden, lässt sich nicht einfach mit anderen Datenquellen (Netzwerk, SharedPreferences) kombinieren.
\n\nAnsatz 2: Coroutines – moderner Kotlin-Weg
\n\nWenn Sie Kotlin Coroutines verwenden (und das sollten Sie), ist dies der eleganteste Weg. Die Funktion wird als suspend markiert, und der Aufruf wird in eine Coroutine mit dem richtigen Dispatchers.IO verpackt.
Schritt 1: DAO mit suspend
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User\n}\n\nSchritt 2: Aufruf aus ViewModel (empfohlen)
\n\nclass MainVi