Android Coden
Android 7 min lesen

Room-Überblick

Room ordnet strukturierte lokale Daten sauber ein. Du lernst, wie Entities und DAO SQLite sicher nutzbar machen.

Room ist für Android-Apps der Standardweg, wenn du strukturierte Daten lokal speichern willst, ohne direkt mit rohem SQLite-Code zu arbeiten. Du modellierst Tabellen als Kotlin-Klassen, beschreibst Datenbankzugriffe in DAO-Interfaces und bekommst dafür eine API, die sich gut in Repositorys, Coroutines, Flow und Jetpack Compose einfügt.

Was ist das?

Room ist eine Jetpack-Bibliothek, die auf SQLite aufsetzt. SQLite ist die lokale relationale Datenbank, die auf Android-Geräten verfügbar ist. Du kannst sie direkt verwenden, müsstest dann aber SQL-Strings, Cursor, Mapping-Code und viele Fehlerfälle selbst verwalten. Room nimmt dir diese Schicht nicht vollständig ab, sondern macht sie strukturierter: Du schreibst weiterhin bewusst Datenmodelle und SQL-Abfragen, aber Room prüft viele Dinge schon beim Build.

Das zentrale Modell besteht aus drei Bausteinen. Eine Entity beschreibt eine Tabelle. Jede Instanz dieser Klasse entspricht meistens einer Zeile. Ein DAO, also Data Access Object, beschreibt die erlaubten Lese- und Schreibzugriffe. Die Database-Klasse verbindet Entities und DAOs zu einer konkreten Datenbank. Dieses Modell hilft dir, lokalen Speicher nicht als ungeordnete Ablage zu behandeln, sondern als klar definierte Datenquelle in der Data Layer deiner App.

Im Android-Alltag ist Room wichtig, sobald Daten nicht nur temporär im Speicher liegen sollen. Beispiele sind gespeicherte Notizen, Favoriten, Suchverläufe, Warenkörbe, gecachte API-Antworten oder Aufgabenlisten. Gerade in Offline-First-Architekturen ist Room oft der lokale Kern: Die UI liest aus der lokalen Datenbank, während Netzwerk-Synchronisation im Hintergrund Daten aktualisiert. Dadurch bleibt die App bedienbar, auch wenn die Verbindung schlecht ist.

Für Lernende ist das richtige mentale Modell entscheidend: Room ist keine magische Objekt-Datenbank. Es ist eine typsichere Schicht über SQLite. Du solltest also weiter in Tabellen, Primärschlüsseln, Spalten, Beziehungen und Abfragen denken. Kotlin-Klassen machen das Modell lesbarer, aber die Daten liegen relational in SQLite.

Wie funktioniert es?

Room arbeitet stark mit Annotationen und Codegenerierung. Du markierst eine Kotlin-Klasse mit @Entity, definierst einen Primärschlüssel und gibst Felder an, die als Spalten gespeichert werden. Room erzeugt daraus kein neues Datenbankprinzip, sondern eine SQLite-Tabelle mit passender Struktur.

Das DAO ist der wichtigste Zugriffspunkt im Code. Dort definierst du Methoden mit Annotationen wie @Query, @Insert, @Update oder @Delete. Bei @Query schreibst du SQL. Der Unterschied zu rohem SQLite liegt in der Prüfung: Room validiert die Query gegen deine Entities. Wenn du eine Spalte falsch schreibst oder einen Rückgabetyp wählst, der nicht zur Query passt, bekommst du einen Build-Fehler statt einen schwer reproduzierbaren Laufzeitfehler.

Die Database-Klasse ist mit @Database annotiert. Sie nennt die beteiligten Entities, die Datenbankversion und stellt DAO-Instanzen bereit. In einer gut geschnittenen App liegt diese Klasse meist in der Data Layer. Repositorys verwenden die DAOs und geben Daten an ViewModels weiter. Die UI, etwa eine Compose-Oberfläche, sollte nicht direkt SQL oder DAO-Details kennen. So bleibt die App testbarer und Änderungen an der Speicherlogik wandern nicht durch den gesamten Code.

Room unterstützt Coroutines und Flow sehr gut. Schreibzugriffe kannst du als suspend-Funktionen definieren. Lesezugriffe, die sich automatisch aktualisieren sollen, gibst du oft als Flow<List<Entity>> zurück. Wenn sich die Tabelle ändert, sendet Room neue Werte. In Compose kann ein ViewModel diesen Flow in UI-State umwandeln. Die Oberfläche reagiert dann auf Datenänderungen, ohne selbst die Datenbank abzufragen.

Wichtig ist auch der Lebenszyklus der Datenbank. Du erstellst die Room-Datenbank normalerweise einmal als Singleton, etwa über Dependency Injection. Wenn jede View oder jedes Repository eine eigene Instanz baut, verschwendest du Ressourcen und riskierst schwer verständliches Verhalten. Außerdem gehören Datenbankzugriffe nicht auf den Main Thread. Room schützt dich in vielen Fällen davor, aber du solltest die Regel verinnerlichen: Datenbankarbeit ist I/O und gehört in passende Coroutine-Kontexte oder in Room-APIs, die dafür ausgelegt sind.

Ein weiterer Kernpunkt sind Migrationen. Sobald du eine App veröffentlichst und später eine Entity änderst, ändert sich die Datenbankstruktur. Nutzerinnen und Nutzer haben aber bereits eine alte lokale Datenbank auf dem Gerät. Dann brauchst du eine Migration von Version 1 zu Version 2, statt die Datenbank still zu löschen. Für Lernprojekte wirkt das übertrieben, in realen Apps ist es Release-Qualität.

In der Praxis

Ein kleines Beispiel: Du baust eine App, die Artikel als Favoriten speichert. Dafür brauchst du eine Tabelle, ein DAO und eine Datenbank. Die Entity bleibt bewusst nah an der lokalen Datenstruktur. Sie ist nicht automatisch dasselbe wie dein Netzwerk-DTO oder dein UI-Modell.

import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomDatabase
import kotlinx.coroutines.flow.Flow

@Entity(tableName = "favorite_articles")
data class FavoriteArticleEntity(
    @PrimaryKey val id: String,
    val title: String,
    val savedAtMillis: Long
)

@Dao
interface FavoriteArticleDao {
    @Query("SELECT * FROM favorite_articles ORDER BY savedAtMillis DESC")
    fun observeFavorites(): Flow<List<FavoriteArticleEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun save(article: FavoriteArticleEntity)

    @Query("DELETE FROM favorite_articles WHERE id = :id")
    suspend fun remove(id: String)
}

@Database(
    entities = [FavoriteArticleEntity::class],
    version = 1
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun favoriteArticleDao(): FavoriteArticleDao
}

Dieses Beispiel zeigt die typische Arbeitsweise. Die Entity definiert, was gespeichert wird. Das DAO definiert, wie darauf zugegriffen wird. Die Query ist normales SQL, aber Room prüft sie. Wenn du savedAtMillis in der Entity umbenennst und die Query nicht anpasst, fällt das beim Build auf. Genau dieser frühe Fehler ist einer der größten Vorteile von Room.

In einer echten App würdest du das DAO nicht direkt aus einem Composable verwenden. Ein Repository könnte observeFavorites() aufrufen und die Daten bei Bedarf in Domain-Modelle umwandeln. Das ViewModel sammelt den Flow und stellt der UI einen stabilen State bereit. Compose zeigt nur an, was im State steht. So bleibt jede Schicht verständlich: Room speichert, das Repository koordiniert, das ViewModel bereitet UI-State vor, Compose rendert.

Eine gute Entscheidungsregel lautet: Verwende Room, wenn deine lokalen Daten eine klare Struktur haben und du gezielt danach fragen willst. Wenn du nur einen kleinen Schalter, ein Token oder eine einzelne Einstellung speicherst, ist DataStore oft passender. Wenn du Listen, Beziehungen, Filter, Sortierung oder lokale Caches brauchst, ist Room meist die bessere Wahl.

Eine typische Stolperfalle ist das Vermischen von Modellen. Viele Anfänger verwenden dieselbe Datenklasse für Netzwerk, Datenbank und UI. Das spart am Anfang Code, koppelt aber alles eng zusammen. Ändert sich der API-Response, kann plötzlich deine Datenbankstruktur betroffen sein. Ändert sich die UI, passt dein Persistenzmodell vielleicht nicht mehr. Besser ist eine saubere Grenze: DTOs für Netzwerkdaten, Entities für Room, UI-Modelle für Darstellung. Bei kleinen Apps kannst du pragmatisch bleiben, aber du solltest die Kopplung bewusst erkennen.

Eine zweite Stolperfalle sind unüberlegte Migrationen. fallbackToDestructiveMigration() kann in frühen Prototypen bequem wirken, löscht aber lokale Daten, wenn sich das Schema ändert. In einer veröffentlichten App ist das meist nicht akzeptabel. Prüfe bei jeder Entity-Änderung, ob die Datenbankversion steigen muss und welche Migration nötig ist. Code-Reviews sollten genau darauf achten.

Du kannst dein Verständnis praktisch testen, indem du eine kleine Tabelle anlegst, eine fehlerhafte Query einbaust und den Build beobachtest. Danach schreibst du einen DAO-Test mit einer In-Memory-Datenbank: speichern, lesen, löschen, Ergebnis prüfen. Wenn du mit Flow arbeitest, prüfe außerdem, ob nach einem Insert wirklich ein neuer Listenwert ankommt. Das gibt dir ein klares Gefühl dafür, wo Room statisch prüft und wo du Verhalten selbst testen musst.

Fazit

Room gibt dir für SQLite eine klare, moderne Struktur: Entities beschreiben lokale Tabellen, DAOs bündeln die erlaubten Zugriffe, und die Database-Klasse verbindet alles zu einer nutzbaren Datenquelle in der Android-Architektur. Für reale Apps zählt dabei nicht nur, dass Daten gespeichert werden, sondern dass Zugriffe nachvollziehbar, testbar und bei Änderungen kontrollierbar bleiben. Nimm dir als Übung eine kleine Funktion deiner App, speichere sie mit Room, prüfe die DAO-Methoden mit Tests und achte im Code-Review besonders auf Query-Namen, Modellgrenzen und Migrationen.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.