androidengineers.Book a session

Android Platform Services

Widgets & Glance Basics

article20 minHard

Home screen widgets let users interact with your app without opening it. Jetpack Glance provides a Compose-like API for building widgets, replacing the old RemoteViews boilerplate.

Glance vs RemoteViews

RemoteViewsGlance
APIXML layouts + method callsCompose-like declarative API
ComplexityHigh boilerplateMuch simpler
State managementManualBuilt-in
CoroutinesExternal wrapper neededNative support
Min SDKAPI 1API 23+

Setup

// implementation("androidx.glance:glance-appwidget:1.1.0")
// implementation("androidx.glance:glance-material3:1.1.0")

Creating a Widget

class ArticleWidget : GlanceAppWidget() {

    override val stateDefinition = ArticleWidgetStateDefinition()

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            val state = currentState<ArticleWidgetState>()
            ArticleWidgetContent(state)
        }
    }
}

@Composable
fun ArticleWidgetContent(state: ArticleWidgetState) {
    GlanceTheme {
        when (state) {
            is ArticleWidgetState.Loading -> WidgetLoadingView()
            is ArticleWidgetState.Error -> WidgetErrorView()
            is ArticleWidgetState.Success -> WidgetSuccessView(state.articles)
        }
    }
}

@Composable
fun WidgetSuccessView(articles: List<ArticleSummary>) {
    Column(
        modifier = GlanceModifier
            .fillMaxSize()
            .background(GlanceTheme.colors.background)
            .padding(16.dp)
            .appWidgetBackground()
            .cornerRadius(16.dp)
    ) {
        Text(
            "Latest Articles",
            style = TextStyle(
                fontWeight = FontWeight.Bold,
                fontSize = 14.sp,
                color = GlanceTheme.colors.onBackground
            )
        )

        Spacer(GlanceModifier.height(8.dp))

        articles.take(3).forEach { article ->
            Row(
                modifier = GlanceModifier
                    .fillMaxWidth()
                    .clickable(
                        actionStartActivity<MainActivity>(
                            actionParametersOf(ActionParameters.Key<String>("article_id") to article.id)
                        )
                    )
                    .padding(vertical = 4.dp)
            ) {
                Text(
                    article.title,
                    style = TextStyle(fontSize = 12.sp, color = GlanceTheme.colors.onBackground),
                    maxLines = 2
                )
            }
        }

        Spacer(GlanceModifier.defaultWeight())

        Row(modifier = GlanceModifier.fillMaxWidth(), horizontalAlignment = Alignment.End) {
            Image(
                provider = ImageProvider(R.drawable.ic_refresh),
                contentDescription = "Refresh",
                modifier = GlanceModifier.clickable(actionRunCallback<RefreshArticlesCallback>())
            )
        }
    }
}

Widget State

// State definition
class ArticleWidgetStateDefinition : GlanceStateDefinition<ArticleWidgetState> {
    override val dataStore: DataStore<Preferences> get() = TODO("inject DataStore")
    override suspend fun getDataStore(context: Context, fileKey: FileKey): DataStore<Preferences> =
        context.dataStore

    override fun getLocation(context: Context, fileKey: FileKey) = TODO()
}

sealed class ArticleWidgetState {
    object Loading : ArticleWidgetState()
    object Error : ArticleWidgetState()
    data class Success(val articles: List<ArticleSummary>) : ArticleWidgetState()
}

Widget Receiver

class ArticleWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = ArticleWidget()

    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        // Schedule data refresh
        enqueueRefreshWork(context)
    }
}

// Declare in AndroidManifest.xml
<receiver
    android:name=".widget.ArticleWidgetReceiver"
    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/article_widget_info" />
</receiver>

Widget Info XML

<!-- res/xml/article_widget_info.xml -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="250dp"
    android:minHeight="110dp"
    android:targetCellWidth="4"
    android:targetCellHeight="2"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="280dp"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="1800000"
    android:previewLayout="@layout/widget_preview"
    android:initialLayout="@layout/widget_loading"
    android:description="@string/widget_description"
    android:widgetCategory="home_screen" />

Refreshing Widget Data

// From anywhere in the app — refresh all widget instances
suspend fun refreshWidgets(context: Context) {
    val manager = GlanceAppWidgetManager(context)
    val ids = manager.getGlanceIds(ArticleWidget::class.java)

    ids.forEach { id ->
        updateAppWidgetState(context, ArticleWidgetStateDefinition(), id) { state ->
            ArticleWidgetState.Loading
        }
        ArticleWidget().update(context, id)
    }
}

// Action callback (called when user taps refresh button in widget)
class RefreshArticlesCallback : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        refreshWidgets(context)
        // Actual data fetch scheduled via WorkManager
        enqueueRefreshWork(context)
    }
}

Key Takeaways

ConceptRule
GlanceAppWidgetMain class; provideGlance renders the UI
GlanceAppWidgetReceiverReceives system broadcasts; triggers initial update
Widget stateUse GlanceStateDefinition + DataStore for persistence
actionStartActivityDeep link from widget tap to specific screen
actionRunCallbackTrigger code without opening the app (e.g., mark done, refresh)
updatePeriodMillisMax 30 minutes (1,800,000ms) — system ignores shorter intervals

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Widgets & Glance Basics | Android System Design | Android Engineers