MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Home screen widgets are one of the most underused retention tools in Android development. Glance brings Compose-like syntax to widget development. Here's how to build a functional widget from scratch.
On this page
Widgets live on the home screen. Your app doesn't need to be open. Users interact with your content without launching anything. Done well, they're a retention multiplier.
Glance (Jetpack Glance) brings Compose-like syntax to
RemoteViewsRemoteViewsRemoteViews// build.gradle.kts
dependencies {
implementation("androidx.glance:glance-appwidget:1.1.0")
}Every widget needs an
AppWidgetProviderGlanceAppWidgetclass TaskWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val tasks = TaskRepository.getInstance(context).getActiveTasks()
provideContent {
TaskWidgetContent(tasks = tasks)
}
}
}And a
GlanceAppWidgetReceiverclass TaskWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = TaskWidget()
}Register both in
AndroidManifest.xml<receiver
android:name=".widgets.TaskWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/task_widget_info" />
</receiver><!-- res/xml/task_widget_info.xml -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp"
android:minHeight="110dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1800000"
android:previewImage="@drawable/widget_preview"
android:widgetCategory="home_screen" />updatePeriodMillisWorkManagerGlance provides its own set of composables — they look similar to Compose but operate differently:
@Composable
fun TaskWidgetContent(tasks: List<Task>) {
GlanceTheme {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(16.dp)
.appWidgetBackground()
) {
Text(
text = "Tasks",
style = TextStyle(
color = GlanceTheme.colors.onSurface,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
)
Spacer(modifier = GlanceModifier.height(8.dp))
if (tasks.isEmpty()) {
Text(
text = "All done! 🎉",
style = TextStyle(
color = GlanceTheme.colors.onSurfaceVariant,
fontSize = 12.sp
)
)
} else {
tasks.take(3).forEach { task ->
TaskRow(task = task)
Spacer(modifier = GlanceModifier.height(4.dp))
}
if (tasks.size > 3) {
Text(
text = "+${tasks.size - 3} more",
style = TextStyle(
color = GlanceTheme.colors.onSurfaceVariant,
fontSize = 11.sp
)
)
}
}
}
}
}
@Composable
fun TaskRow(task: Task) {
Row(
modifier = GlanceModifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Image(
provider = ImageProvider(
if (task.completed) R.drawable.ic_check_circle else R.drawable.ic_circle
),
contentDescription = null,
modifier = GlanceModifier.size(16.dp)
)
Spacer(modifier = GlanceModifier.width(8.dp))
Text(
text = task.title,
style = TextStyle(
color = GlanceTheme.colors.onSurface,
fontSize = 12.sp
),
maxLines = 1
)
}
}Widget elements can open the app or trigger actions:
// Open app on widget tap
Column(
modifier = GlanceModifier
.clickable(actionStartActivity<MainActivity>())
) { ... }
// Complete a task from the widget
Image(
provider = ImageProvider(R.drawable.ic_circle),
contentDescription = "Complete task",
modifier = GlanceModifier.clickable(
actionRunCallback<CompleteTaskAction>(
actionParametersOf(
ActionParameters.Key<String>("task_id") to task.id
)
)
)
)
// Action handler
class CompleteTaskAction : ActionCallback {
override suspend fun onAction(
context: Context,
glanceId: GlanceId,
parameters: ActionParameters
) {
val taskId = parameters[ActionParameters.Key<String>("task_id")] ?: return
TaskRepository.getInstance(context).completeTask(taskId)
TaskWidget().update(context, glanceId)
}
}The widget needs to be updated when data changes:
// From anywhere in your app
suspend fun updateAllTaskWidgets(context: Context) {
GlanceAppWidgetManager(context)
.getGlanceIds(TaskWidget::class.java)
.forEach { glanceId ->
TaskWidget().update(context, glanceId)
}
}
// Call this after creating/completing/deleting tasks
viewModelScope.launch {
repository.completeTask(taskId)
updateAllTaskWidgets(context)
}Glance maps to
RemoteViewsLazyColumnLazyColumnLazyColumnIf you need a live, frequently-updating widget, use
WorkManagerupdatePeriodMillisGlanceAppWidgetGlanceAppWidgetReceiveractionStartActivityactionRunCallbackglanceAppWidget.update()Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read