stateless Todo-Items und ViewModel
Dauer: 30 Minuten
data class Todo
als Modell- State-Quelle im Screen statt in
setContent
- Stateless
TodoItem
(state ↓ / events ↑) - ViewModel einführen
Ziel: State-Management und Architektur der App verbessern
Wir bauen die Todos-App so um, dass zunächst ein Composable den Bildschirmzustand hält (Single Source of Truth). Später wird dieser Zustand in ein ViewModel ausgelagert. Jedes Listen-Item ist zustandslos (stateless): Es bekommt Status hinein und sendet Ereignisse hinaus.
State in Screen-Composable halten
Statt State in setContent
zu halten, kapseln wir ihn in einem Screen-Composable TodosApp
. Zusätzlich entfernen wir die bisherigen vorgegeben Todos und starten mit einer leeren Liste.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TodosApp()
}
}
}
@Composable
fun TodosApp() {
val todos = remember { mutableStateListOf<String>() }
var showDialog by remember { mutableStateOf(false) }
TodosAndroid2025Theme {
Scaffold(floatingActionButton = {
FloatingActionButton(
onClick = { showDialog = true },
) {
Icon(
painter = painterResource(R.drawable.add_24px),
contentDescription = "Neues Todo erstellen"
)
}
}) { innerPadding ->
if (showDialog) {
AddTodoDialog(
onAdd = { text ->
todos.add(text)
showDialog = false
},
onDismiss = { showDialog = false }
)
}
TodoList(todos = todos, modifier = Modifier.padding(innerPadding))
}
}
}
Datenmodell mit data class
Wir nutzen ein einfaches Model für Todos, um die Datenstruktur klar zu definieren. Dies wird uns später u.a. bei der Erstellung der Datenbank helfen.
Dazu nutzen wir data class
aus Kotlin. Jedes Todo hat zunächst einen Text
und einen Erledigt-Status. Später erhält ein Todo auch eine eindeutige ID.
Folgende neue Datei Todo.kt
im data
-Package erstellen (weitere Klassen kommen später dazu):
data class Todo(val text: String, val done: Boolean = false)
Eine data class
erzeugt automatisch die getter sowie einige
nützliche Methoden wie copy
, toString
, equals
und hashCode
.
Durch die Einführung einer data class
bzw. eines Models namens Todo
müssen wir die bisherigen Stellen in der MainActivity
anpassen, die mit
String
-Todos gearbeitet haben. Außerdem nennen wir die Composable für
ein einzelnes Todo in TodoItem
um (statt wie bisher Todo
).
@Composable
fun TodosApp() {
val todos = remember { mutableStateListOf<Todo>() }
// restlicher Code unverändert
if (showDialog) {
AddTodoDialog(
onAdd = { text ->
todos.add(Todo(text=text))
showDialog = false
},
onDismiss = { showDialog = false }
)
}
// restlicher Code unverändert
}
@Composable
fun TodoItem(todo: Todo) {
var done by remember { mutableStateOf(todo.done) }
Row(
// restlicher Code unverändert
) {
// restlicher Code unverändert
Text(todo.text, fontWeight = FontWeight.Bold, fontSize = 22.sp)
}
}
@Composable
fun TodoList(todos: List<Todo>, modifier: Modifier = Modifier) {
LazyColumn(modifier) {
items(todos) { todo ->
TodoItem(todo)
}
}
}
Wir haben auch das Preview
-Composable weggelassen, um dies nicht
anpassen zu müssen.
TodoItem
wird stateless
In Compose sollten Composables möglichst stateless sein, also keinen eigenen Zustand halten. Dadurch sind sie wiederverwendbar und einfacher zu testen.
Wir führen in TodosApp
eine Funktion toggleAt
ein, die den
Status eines Todos ändert. Diese Funktion wird an zuerst an die TodoList
übergeben und von dort an TodoItem
weitergereicht.
fun TodosApp() {
val todos = remember { mutableStateListOf<Todo>() }
var showDialog by remember { mutableStateOf(false) }
fun toggleAt(index: Long) {
// Long macht spätere Umstellung auf ID einfacher
val idx = index.toInt()
todos[idx] = todos[idx].copy(done = !todos[idx].done)
}
// restlicher Code unverändert
TodoList(
todos = todos,
onToggleAt = ::toggleAt,
modifier = Modifier.padding(innerPadding)
)
// restlicher Code unverändert
}
@Composable
fun TodoList(todos: List<Todo>, onToggleAt: (Long) -> Unit, modifier: Modifier = Modifier) {
LazyColumn(modifier) {
itemsIndexed(todos) { index, todo ->
TodoItem(todo = todo, onCheckedChange = { onToggleAt(index.toLong()) })
}
}
}
@Composable
fun TodoItem(todo: Todo, onCheckedChange: (Boolean) -> Unit) {
Row(
modifier = Modifier
.toggleable(value = todo.done, onValueChange = onCheckedChange, role = Role.Checkbox)
.padding(10.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = todo.done, onCheckedChange = null)
Text(todo.text, fontWeight = FontWeight.Bold, fontSize = 22.sp)
}
}
Wir nutzen itemsIndexed
in der LazyColumn
, um den Index des aktuellen
Todos zu erhalten. Den Index eines Todos in LazyColumn
übergeben wir an die
onToggleAt
-Funktion (deklariert in TodosApp
), die den Status des Todos ändert.
Später werden wir die Index-basierte Logik durch eine ID-basierte Logik
ersetzen, wenn wir dem Todo
-Model eine ID hinzufügen und mit einer
Datenbank arbeiten. Daher verwenden wir als Parameter für toggleAt
von Anfang an Long
statt Int
.
Warum so?
- Eine Single Source of Truth für den State vereinfacht Testbarkeit & Erweiterbarkeit.
- Composables wie
TodoItems
sind stateless und somit wiederverwendbar. - Änderungen (z. B. Sortieren/Filtern) sind zentral lösbar.
ViewModel einführen
Wir verlagern die Quelle der Wahrheit aus dem UI-Code von TodosApp
in ein separates ViewModel. So übersteht der Datenzustand Konfigurationswechsel (z. B. Rotation) und die UI bleibt schlank (separation of concerns).
Das ViewModel wird im ui
-Package abgelegt. Dazu eine neue Datei TodosViewModel.kt
erstellen:
// package und import statements weggelassen
class TodosViewModel : ViewModel() {
private val _todos = mutableStateListOf<Todo>()
val todos: List<Todo> get() = _todos // read-only nach außen
fun add(text: String) {
val t = text.trim()
if (t.isNotEmpty()) _todos += Todo(t, false)
}
fun toggleAt(index: Long) {
val idx = index.toInt() // Long macht spätere Umstellung auf ID einfacher
_todos[idx] = _todos[idx].copy(done = !_todos[idx].done)
}
}
Ein ViewModel trennt die UI-Schicht von der Datenhaltung. Es ist die zentrale Quelle für den Todo-Zustand; die UI liest diesen und ruft nur Funktionen zum Ändern auf (Hinzufügen/Umschalten).
TodosApp
an ViewModel binden
TodosApp
bekommt eine ViewModel-Instanz injiziert, die mit viewModel()
erstellt wird. Dadurch wird das ViewModel automatisch an den Lebenszyklus der
MainActivity gebunden. Das ViewModel wird als private Variable in der
MainActivity
deklariert, damit es nur dort erstellt wird und an TodosApp
weitergereicht werden kann:
class MainActivity : ComponentActivity() {
private val vm: TodosViewModel by viewModels()
// restlicher Code bleibt unverändert
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TodosApp(vm)
}
}
}
Danach können wir das ViewModel in TodosApp
verwenden und den bisherigen State
entfernen. Außerdem müssen wir die Funktionsaufrufe an das ViewModel anpassen:
@Composable
fun TodosApp(vm: TodosViewModel) {
val todos = vm.todos
// restlicher Code unverändert
if (showDialog) {
AddTodoDialog(
onAdd = { text ->
vm.add(text)
showDialog = false
},
onDismiss = { showDialog = false }
)
}
TodoList(
todos = todos,
onToggleAt = vm::toggleAt,
modifier = Modifier.padding(innerPadding)
)
// restlicher Code unverändert
}
ViewModels zahlen sich in der Android-Entwicklung besonders bei komplexeren Apps aus, die z.B. mit einer Datenbank arbeiten (siehe nächste Abschnitte).
Kurze Bemerkungen:
- ViewModel lebt außerhalb des Compose-UIs und braucht kein
remember
. remember
dient dazu, den Zustand lokal innerhalb in einer Composable-Funktion zu speichern, während das ViewModel den Zustand über die gesamte Lebensdauer der App hinweg vorhält.- Der State in ViewModel ist nicht automatisch „reaktiv“. Hier nutzen wir
mutableStateListOf
, damit Änderungen an der Liste von Compose erkannt werden. - ViewModel kann auch andere State-Container wie
LiveData
oderStateFlow
verwenden (siehe weiterführende Links unten).
Hier kann nur ein grober Einstieg in ViewModel gegeben werden. Das Thema ViewModel und Architektur-Patterns (Repository, LiveData, StateFlow, …) ist sehr umfangreich und entwickelt sich ständig weiter.
Mehr dazu:
- App Architecture: https://developer.android.com/topic/architecture
- ViewModel: https://developer.android.com/topic/libraries/architecture/viewmodel
- State and Jetpack Compose: https://developer.android.com/develop/ui/compose/state
→ Bei Codelabs, Tutorials und Youtube-Videos auf das Datum achten!