Verwendung von Room

💡

Dauer: 45 Minuten

  • Room-Datenbank erstellen
  • Verwendung der Room-Datenbank in der Todo-App

Ziel: Datenbankzugriff mit Room in Android verstehen

Room-Datenbank erstellen

Wir erstellen eine abstrakte Klasse data/AppDatabase, die die Datenbank konfiguriert:

import androidx.room.Database
import androidx.room.RoomDatabase
 
@Database(entities = [TodoItem::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao
}

Datenbankinstanz initialisieren

Wir initialisieren die Room-Datenbank in der MainActivity:

class MainActivity : ComponentActivity() {
    private lateinit var database: AppDatabase
    private lateinit var todoDao: TodoDao
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        // Datenbank erstellen
        database = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "todo-database"
        ).build()
 
        todoDao = database.todoDao()
 
        // Restlicher Code bleibt unverändert...
    }
}

Todos aus der Datenbank laden und im UI anzeigen

In der MainActivity ersetzen wir den State todos durch die aus der Datenbank geladenen Todos:

setContent {
    val todos by todoDao.getAllTodos().collectAsState(initial = emptyList())
    // Restlicher Code bleibt unverändert...
}

Nun verwenden wir die data class namens TodoItem anstelle von String für die Todos. Dies zieht einige Änderungen im UI-Code in der MainActivity nach sich:

Speicherung des Todos im AddTodoDialog mit Hilfe des Todo-DAOs:

if (showDialog) {
    AddTodoDialog(
        onDismiss = { showDialog = false },
        onAdd = { newTodo ->
            lifecycleScope.launch {
                todoDao.insertTodo(TodoItem(name = newTodo))
            }
            showDialog = false
        }
    )
}

Die TodoList muss nun eine Liste von TodoItems als Parameter annehmen:

@Composable
fun TodoList(todos: List<TodoItem>, modifier: Modifier = Modifier) {
    LazyColumn(modifier) {
        items(todos) { todo ->
            Todo(todo)
        }
    }
}

Das Todo-Composable muss ebenfalls angepasst werden (TodoItem als Parameter mit entsprechenden Zugriffen auf die Properties):

@Composable
fun Todo(todo: TodoItem) {
    var done by remember { mutableStateOf(todo.done) }
    Row(
        modifier = Modifier
            .clickable(onClick = { done = !done })
            .padding(10.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(checked = done, onCheckedChange = { done = it })
        Text(todo.name, fontWeight = FontWeight.Bold, fontSize = 22.sp)
    }
}

Die Preview des Todo-Composables (TodoPreview) löschen wir, damit wir diese nicht anpassen müssen (Previews sind optional).

Wir können jetzt Todos erstellen, die in der Datenbank gespeichert werden und im UI angezeigt werden.

💡

Wir benötigen lifecycleScope.launch zum Speichern, weil ein Datenbank-Zugriffe asynchron sind.

In Android Studio kann auf die Datenbank eines Emulators im Database Inspector zugegriffen werden. Dazu View > Tool Windows > App Inspection öffnen.

Todos in der Datenbank aktualisieren

Wir reichen das todoDao an Todo weiter:

// in setContent von onCreate:
TodoList(
    todos = todos,
    todoDao = todoDao,
    modifier = Modifier.padding(innerPadding)
)
 
// in TodoList:
@Composable
fun TodoList(todos: List<TodoItem>, todoDao: TodoDao, modifier: Modifier = Modifier) {
    LazyColumn(modifier) {
        items(todos) { todo ->
            Todo(todo = todo, todoDao = todoDao)
        }
    }
}

Das Todo-Composable wird dann so angepasst:

@Composable
fun Todo(todo: TodoItem, todoDao: TodoDao) {
    val coroutineScope = rememberCoroutineScope()
    var done by remember { mutableStateOf(todo.done) }
    Row(
        modifier = Modifier
            .clickable(onClick = {
                done = !done
                // Status in der Datenbank aktualisieren
                coroutineScope.launch {
                    todoDao.updateTodo(todo.copy(done = done))
                }
            })
            .padding(10.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(checked = done, onCheckedChange = {
            done = it
            // Status in der Datenbank aktualisieren
            coroutineScope.launch {
                todoDao.updateTodo(todo.copy(done = done))
            }
        })
        Text(todo.name, fontWeight = FontWeight.Bold, fontSize = 22.sp)
    }
}

Nun wird der Status eines Todos (erledigt oder nicht) mit der Datenbank synchronisiert.

⚠️

Die Verwendung von DAOs in Composable mit lifecycleScope.launch und coroutineScope.launch entspricht nicht den best practices in Android. Wir haben diese hier nur aus Gründen der Einfachheit verwendet. UI-Logik und Datenbankzugriffe sollten in komplexeren Apps getrennt werden.

Langfristig und in komplexeren Android-Apps sollten hierzu weitere Konzepte wie das Repository-Pattern, ViewModel und LiveData eingesetzt werden, um die Datenbankzugriffe vom UI zu trennen und den Code klarer zu strukturieren. Dazu finden sich Beispiele in der Android-Dokumentation, in Codelabs und in Sample-Apps (siehe unten).

Abschließende Bemerkungen

Der konzeptuelle Aufbau von Room wirkt anfangs etwas komplex (siehe grafische Darstellung des Aufbaus von Room in der Dokumentation).

Sobald Room jedoch eingerichtet ist, können Datenbankoperationen einfach und effizient durchgeführt werden. Room bietet eine umfangreiche API, um Datenbankzugriffe zu verwalten und zu optimieren.

Vertiefendes Material

Guide in Android Developer Docs zum Thema Daten und Dateien https://developer.android.com/guide/topics/data

Room in den Android Developer Docs:

⚠️

Achtung

Wir verwenden zusätzlich zu app/build.gradle die Datei gradle/lib.version.toml, um die Versionen von Bibliotheken zu verwalten. In der Dokumentation wird oft noch empfohlen, die Versionen ausschließlich in app/build.gradle zu spezifizieren.

Room entwickelt sich ständig weiter… (wie alles andere in Android…)