Skip to content
All posts
March 29, 20263 min read

Android Widgets With Glance: Building Home Screen Widgets in Compose

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.

AndroidJetpack ComposeKotlin
Share:

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

code
RemoteViews
-based widget development. It's not full Compose — the underlying system is still
code
RemoteViews
— but it's dramatically better than writing
code
RemoteViews
XML.


Setup

kotlin
// build.gradle.kts
dependencies {
    implementation("androidx.glance:glance-appwidget:1.1.0")
}

The Widget Provider

Every widget needs an

code
AppWidgetProvider
equivalent. In Glance, it's a
code
GlanceAppWidget
:

kotlin
class TaskWidget : GlanceAppWidget() {
    
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val tasks = TaskRepository.getInstance(context).getActiveTasks()
        
        provideContent {
            TaskWidgetContent(tasks = tasks)
        }
    }
}

And a

code
GlanceAppWidgetReceiver
:

kotlin
class TaskWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = TaskWidget()
}

Register both in

code
AndroidManifest.xml
:

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>

Widget Info XML

xml
<!-- 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" />

code
updatePeriodMillis
is the automatic update interval. Minimum is 1,800,000ms (30 minutes) — the system enforces this. For more frequent updates, use
code
WorkManager
.


Building the Widget UI With Glance

Glance provides its own set of composables — they look similar to Compose but operate differently:

kotlin
@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
        )
    }
}

Handling Clicks

Widget elements can open the app or trigger actions:

kotlin
// 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)
    }
}

Updating the Widget

The widget needs to be updated when data changes:

kotlin
// 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)
}

What Glance Can't Do

Glance maps to

code
RemoteViews
— the underlying system hasn't changed. Limitations:

  • No gestures (swipe, pinch, etc.)
  • No custom drawing
  • Limited layout options (no
    code
    LazyColumn
    code
    LazyColumn
    analog limited to
    code
    LazyColumn
    in Glance)
  • Updates are asynchronous and can have a delay

If you need a live, frequently-updating widget, use

code
WorkManager
for updates instead of
code
updatePeriodMillis
.


Takeaways

  • Glance brings Compose-like syntax to widgets — dramatically better than raw RemoteViews XML
  • code
    GlanceAppWidget
    +
    code
    GlanceAppWidgetReceiver
    are the two main classes
  • Minimum system update interval is 30 minutes — use WorkManager for more frequent updates
  • Click handlers use
    code
    actionStartActivity
    and
    code
    actionRunCallback
    — not regular click listeners
  • Update the widget explicitly after data changes with
    code
    glanceAppWidget.update()
Share:
S

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.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

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.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this