Widgets mit Glance bauen
Glance modernisiert App-Widgets mit einer Compose-ähnlichen API. Du baust Home-Screen-Widgets in deklarativem Kotlin ohne RemoteViews-Boilerplate.
App-Widgets gehören seit den ersten Android-Versionen zur Plattform – doch die klassische Implementierung über RemoteViews war umständlich, schwer zu debuggen und noch schwerer zu warten. Mit Glance bietet Jetpack eine moderne Alternative: eine deklarative, Compose-ähnliche API, die das Schreiben von Home-Screen-Widgets erheblich vereinfacht. Wer bereits Jetpack Compose kennt, findet sich schnell zurecht, muss aber einige wichtige Unterschiede kennen und verinnerlichen.
Was ist das?
Glance ist eine Jetpack-Bibliothek, die eine Compose-ähnliche API für App-Widgets bereitstellt. Intern setzt sie nach wie vor auf RemoteViews auf – denn Android verlangt das auf Betriebssystemebene –, aber die Konvertierung übernimmt Glance vollständig. Als Entwicklerin oder Entwickler schreibst du deklarativen Kotlin-Code und musst dich nicht mehr um XML-Layouts oder die fehleranfällige RemoteViews-API kümmern.
Wichtig ist jedoch das richtige mentale Modell: Glance ist kein Jetpack Compose. Es teilt denselben deklarativen Stil und eine ähnliche Komponentenhierarchie, aber die Composables sind vollständig verschieden. Du verwendest spezielle Glance-Klassen wie Box, Column, Row, Text, Button und Image aus dem Paket androidx.glance. Normale Compose-Widgets lassen sich hier nicht einfach einsetzen. Wer diesen Unterschied unterschätzt, wird früher oder später mit Kompilierfehlern konfrontiert, die zunächst rätselhaft wirken.
Glance gehört zur Phase der Platform APIs und ergänzt das Werkzeugset um die Fähigkeit, außerhalb der eigenen App präsent zu sein – auf dem Home-Screen des Nutzers, ohne dass die App aktiv geöffnet sein muss.
Wie funktioniert es?
Ein Glance-Widget besteht aus drei Kernbestandteilen, die zusammenspielen.
GlanceAppWidget
Die zentrale Klasse, von der du erbst. Du überschreibst provideGlance() und beschreibst darin die Widget-UI. Diese Funktion wird aufgerufen, wann immer Android das Widget neu rendern muss – etwa nach einem Update oder nach einem Systemneustart.
class TaskWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
TaskWidgetContent()
}
}
}
@Composable
fun TaskWidgetContent() {
Column(
modifier = GlanceModifier.fillMaxSize().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Nächste Aufgabe",
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold)
)
}
}
GlanceAppWidgetReceiver
Du registrierst dein Widget über einen GlanceAppWidgetReceiver. Er verbindet das Android-System mit deiner GlanceAppWidget-Implementierung und muss im Manifest eingetragen werden.
class TaskWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget = TaskWidget()
}
Zustandsverwaltung
Für dynamische Inhalte kannst du Daten direkt in provideGlance() laden – zum Beispiel aus einem Repository oder DataStore. Das entspricht der Data-Layer-Architektur aus den offiziellen Android-Richtlinien: Die UI-Schicht (hier das Widget) fragt Daten beim Repository an, das seinerseits auf Datenbank oder Netzwerk zugreift.
Um ein Widget manuell zu aktualisieren, rufst du TaskWidget().updateAll(context) auf. Das eignet sich gut in Kombination mit WorkManager: Ein periodischer Job holt neue Daten und triggert anschließend das Widget-Update.
In der Praxis
Ein typischer Anwendungsfall ist ein Widget, das die nächste offene Aufgabe aus einer lokalen Room-Datenbank anzeigt:
class TaskWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val repo = TaskRepository.getInstance(context)
val nextTask = repo.getNextPendingTask()
provideContent {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(ColorProvider(Color.White))
.padding(12.dp)
) {
Text(
text = nextTask?.title ?: "Keine offenen Aufgaben",
style = TextStyle(fontSize = 14.sp)
)
Spacer(modifier = GlanceModifier.height(8.dp))
Button(
text = "App öffnen",
onClick = actionStartActivity<MainActivity>()
)
}
}
}
}
Typische Stolperfalle: Klick-Aktionen
Wer Compose gewohnt ist, greift reflexartig zu Modifier.clickable {} – das existiert in Glance nicht in dieser Form. Klick-Aktionen werden über spezialisierte Funktionen definiert:
// Activity öffnen
modifier = GlanceModifier.clickable(actionStartActivity<DetailActivity>())
// Eigene Callback-Klasse ausführen
modifier = GlanceModifier.clickable(actionRunCallback<RefreshAction>())
RefreshAction ist dabei eine Klasse, die ActionCallback implementiert und im Callback-Body z. B. TaskWidget().updateAll(context) aufruft. Dieses Muster ist zunächst ungewohnt, aber konsequent: Widgets laufen in einem anderen Prozesskontext, weshalb direkte Lambda-Closures technisch nicht funktionieren.
Eine zweite häufige Falle ist die Annahme, dass Glance-Widgets in Echtzeit reagieren. Tatsächlich kontrolliert Android, wann ein Widget neu gerendert wird. Plane deshalb keine hochfrequenten Updates ein – das kostet Akku und wird vom System gedrosselt.
Fazit
Glance macht App-Widgets endlich wartbar: Statt RemoteViews-Boilerplate schreibst du deklarativen Kotlin-Code in einem vertrauten Stil. Die wichtigsten Unterschiede zu Jetpack Compose – eigene Komponenten, eine andere Klick-API, kein reaktives State-System im Compose-Sinne – sind nach einem ersten echten Widget schnell verinnerlicht. Baue jetzt ein einfaches Widget für deine nächste App: Beginne mit einem statischen Text, integriere dann einen DataStore-basierten Zustand und beobachte im Emulator, wie sich updateAll() auf den Home-Screen auswirkt. Erst wer den Render-Zyklus selbst erlebt hat, versteht, warum Glance so designt wurde, wie es ist.