Share
Embed
Your app at a Glance: App Widgets in Compose
Widgets have been a part of Android since its beginning, but their diffusion has often been kept back by a complex development process.
That has changed with Jetpack Glance, a modern framework powered by Jetpack Compose that significantly simplifies widget creation.
In this talk, we'll journey through widgets, understanding what they are, what the common use cases are and how they works under the hood.
We'll also dive deep into Jetpack Glance, exploring its features and benefits. But we won't stop at theory – we'll build a widget for an existing app, addressing real-world issues often overlooked in basic documentation.
Fabio Catinella
April 26, 2025
Transcript
78 slides
-
Fabio Catinella Senior Android Developer @ Your App at a
Glance App widgets in Compose -
Summary • What is a widget? • How does it
work? • Making one of it was HARD • Widget and Jetpack Compose • New widget creation • Conclusions -
What is a widget? “Widgets are customizable home screen elements
that display a clear and actionable view of an app's content or actions.” Source: h tt ps://developer.android.com/design/ui/mobile/guides/widgets -
View Layout App Widget Provider Info App Widget Provider What
is a widget? A widget is not a single entity but a group of di ff erent components. -
App Widget Provider Info App Widget Provider View Layout What
is a widget? A widget is not a single entity but a group of di ff erent components. -
Widget Host (Launcher) App Widget Provider Info App Widget Provider
View Layout What is a widget? A widget is not a single entity but a group of di ff erent components. -
AppWidgetProvider Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview The AppWidgetProvider is the core
engine of a Widget. It is responsible for creating the Widget UI as well as for de fi ning what to do when an interaction occurs. It receives broadcast events when the widget is updated, enabled, disabled, or deleted. -
AppWidgetProvider Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview APPWIDGET PROVIDER BROADCAST RECEIVER The
AppWidgetProvider is the core engine of a Widget. It is responsible for creating the Widget UI as well as for de fi ning what to do when an interaction occurs. It receives broadcast events when the widget is updated, enabled, disabled, or deleted. -
AppWidgetProvider Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview Like any other Broadcast Receiver,
the AppWidgetProvider must be declared in the manifest. <receiver android:name=".widget.GameDBWidgetProvider" android:exported="false"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_info" /> </receiver> -
AppWidgetProvider Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview Like any other Broadcast Receiver,
the AppWidgetProvider must be declared in the manifest. <receiver android:name=".widget.GameDBWidgetProvider" android:exported="false"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_info" /> </receiver> -
AppWidgetProvider Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview <receiver android:name=".widget.GameDBWidgetProvider" android:exported="false"> <intent-filter> <action
android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_info" /> </receiver> -
AppWidgetProviderInfo Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview The AppWidgetProviderInfo is a XML
fi le that de fi nes the essential qualities of a widget such as its size. <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:configure="com.example.android.ExampleAppWidgetConfigurationActivity" android:description="@string/example_appwidget_description" android:initialLayout="@layout/example_loading_appwidget" android:maxResizeWidth="250dp" android:maxResizeHeight="120dp" android:minWidth="40dp" android:minHeight="40dp" android:previewLayout="@layout/example_appwidget_preview" android:resizeMode="horizontal|vertical" android:targetCellWidth="1" android:targetCellHeight="1" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen" android:widgetFeatures="reconfigurable|configuration_optional" /> -
AppWidget Manager How does it work? What widgets can I
add? Widget List -
AppWidget Provider AppWidget Manager How does it work? When an
instance of our widget is being added to a widget host, the system sends the APPWIDGET_UPDATE broadcast to our AppWidgetProvider. APPWIDGET_UPDATE (ID) -
How does it work? AppWidget Provider Generated RemoteView + ID
Then the AppWidgetProvider generates a Remote View and, coupled with the ID the system assigned to the widget, sends it to the AppWidgetManager. AppWidget Manager -
How does it work? AppWidget Host (Launcher) Generated RemoteView At
this point the AppWidget Manager sends to the AppWidget Host the new updated Remote View to render. Once the AppWidget Host receives it, it displays it. AppWidget Manager -
Remote Views Source: h tt ps://developer.android.com/reference/android/widget/RemoteViews RemoteViews is an Android
class describing a View displayed in a di ff erent process than the one that created it, which capabilities are intentionally restricted for security reasons. This design aims to prevent potential incidents such as: • Denial of service • Ba tt ery drain • and many others… -
Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview Given the limitations of Remote Views,
some UI building blocks that rely on restricted gestures are not available for widgets. For example, horizontal scrolling blocks (like RecyclerView) are not available. Remote Views -
Making one of it was HARD For time reasons I
won’t go into details, but making widgets was really hard due to the restrictions of RemoteView. Someone referred building widgets as: “Building UIs with Handcu ff s” -
Jetpack Glance Source: h tt ps://developer.android.com/develop/ui/compose/glance “Jetpack Glance is a
framework built on top of the Jetpack Compose runtime that lets you develop and design app widgets using Compose-like APIs.” -
Widget anatomy Source: h tt ps://developer.android.com/develop/ui/views/appwidgets/overview App Widget Provider Info
App Widget Provider View Layout (XML) -
Widget anatomy (with Glance) Source: h tt ps://developer.android.com/develop/ui/compose/glance/create-app-widget App Widget
Provider Info Glance AppWidget Receiver View Layout (Compose) -
GlanceAppWidgetReceiver Source: h tt ps://developer.android.com/develop/ui/compose/glance/create-app-widget class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = TODO("Create GlanceAppWidget") } -
GlanceAppWidget Source: h tt ps://developer.android.com/develop/ui/compose/glance/create-app-widget GlanceAppWidget is an Abstract class
which only needs the function provideGlance to be implemented abstract class GlanceAppWidget(…) { … abstract suspend fun provideGlance( context: Context, id: GlanceId, ) … } -
GlanceAppWidget provideGlance is the function responsible for building the UI
using @Composable components. private class MyWidget : GlanceAppWidget() { override suspend fun provideGlance( context: Context, id: GlanceId ) { provideContent { Text("Hello World") } } } -
GlanceAppWidget As previously stated Widgets have some limitations, for this
reason we cannot use the standard Jetpack Compose components but we need to use Glance ones. import androidx.compose.foundation.layout.Box import androidx.compose.ui.Modifier import androidx.compose.foundation.background import androidx.compose.material3.Text … Box( modifier = Modifier.background(Color.LightGray) ) { Text( text = "Hello World" ) } -
GlanceAppWidget As previously stated Widgets have some limitations, for this
reason we cannot use the standard Jetpack Compose components but we need to use Glance ones. import androidx.glance.layout.Box import androidx.glance.GlanceModifier import androidx.glance.background import androidx.glance.text.Text … Box( modifier = GlanceModifier.background(Color.LightGray) ) { Text( text = "Hello World" ) } -
TheGameDB App It’s just a simple app that uses the
IGDB APIs to display game info. Let’s say we want to add a widget that takes a random game and show it on the home screen. -
Random Game Widget goals For our widget we would like
to achieve these goals: 1) It must be well integrated with the system theme. 2) Display game covers and summaries. 3) Take advantage of the space it is given to it. 4) Open the app when touched. -
Scaffold Sca ff old is a component o ff ered
by Glance Material 3 Library that handles some tricky aspects of the Material 3 design system for us, like the border radius, content padding, title bar and element colors based on the device wallpaper. -
Scaffold import androidx.glance.layout.Box import androidx.glance.GlanceModifier import androidx.glance.background import androidx.glance.text.Text …
Box( modifier = GlanceModifier.background(Color.LightGray) ) { Text( text = "Hello World" ) } -
Scaffold import androidx.glance.layout.Box import androidx.glance.GlanceModifier import androidx.glance.background import androidx.glance.text.Text …
Scaffold( titleBar = { TitleBar( startIcon = ImageProvider(R.drawable.igdb_logo), title = "Random Game" ) } ) { Box( modifier = GlanceModifier.background(Color.LightGray) ) { Text( text = "Hello World" ) } } -
Random Game Widget goals For our widget we would like
to achieve these goals: 1) It must be well integrated with the system theme. 2) Display game covers and summaries. 3) Take advantage of the space it is given to it. 4) Open the app when touched. ✅ -
Let’s add some data private class RandomGameWidget : GlanceAppWidget() {
... override suspend fun provideGlance(context: Context, id: GlanceId) { /* -> Load data here <- */ provideContent{…} } ... } When building a widget using Jetpack Glance, the appropriate place to load the necessary data is within the provideGlance function, which precedes the invocation of provideContent responsible for rendering the widget. -
Let’s add some data “provideGlance is run in the background
as a CoroutineWorker in response to calls to update and updateAll, as well as requests from the Launcher. Before provideContent is called, provideGlance is subject to the typical WorkManager time limit (currently ten minutes). A ft er provideContent is called, the composition continues to run and recompose for about 45 seconds.” Source: h tt ps://developer.android.com/reference/kotlin/androidx/glance/appwidget/GlanceAppWidget -
Let’s add some data private class RandomGameWidget : GlanceAppWidget() {
... override suspend fun provideGlance(context: Context, id: GlanceId) { val gameRepository = GameRepository.getInstance() val game = withContext(Dispatchers.IO) { gameRepository.getRandomGame() } provideContent{ RandomGameWidgetContent(game) } } ... } -
Let’s add some data @Composable internal fun RandomGameWidgetContent( game: GameDetails,
) { … Column { Text( text = game.name, style = TextStyle( fontSize = 25.sp, fontWeight = FontWeight.Bold, color = GlanceTheme.colors.onBackground ) ) Spacer(modifier = GlanceModifier.height(8.dp)) Text( text = (game.summary?.take(137) + "..."), style = TextStyle( color = GlanceTheme.colors.onBackground ) ) } … } -
Adding images To enhance our widget, we aim to display
the cover a rt of the featured video game. In Compose, it's common practice to use Coil, a library providing the AsyncImage component for e ffi cient remote image loading. Unfo rt unately, Coil does not o ff er out-of-the-box suppo rt for Jetpack Glance. 🥲 -
Adding images However, It is still possible to create a
custom component that uses Coil at its core and allows us to load images. -
AsyncWidgetImage @Composable fun AsyncWidgetImage( data: Any?, contentDescription: String?, modifier: GlanceModifier
= GlanceModifier ) { val context = LocalContext.current var bitMap: Bitmap? by remember { mutableStateOf(null) } LaunchedEffect(data) { val request = ImageRequest.Builder(context).data(data).build() bitMap = ImageLoader(context).execute(request).drawable?.toBitmap() } bitMap?.let { Image( provider = ImageProvider(it), contentScale = ContentScale.Crop, contentDescription = contentDescription, modifier = modifier.background(GlanceTheme.colors.primary) ) } } -
AsyncWidgetImage Row { AsyncWidgetImage( data = game.coverUrl, contentDescription = null,
modifier = GlanceModifier .height(135.dp) .width(100.dp) .cornerRadius(8.dp) ) Spacer(modifier = GlanceModifier.width(8.dp)) Column { ... } } -
Random Game Widget goals For our widget we would like
to achieve these goals: 1) It must be well integrated with the system theme. 2) Display game covers and summaries. 3) Take advantage of the space it is given to it. 4) Open the app when touched. ✅ ✅ -
Adaptiveness To get an high quality widget it is impo
rt ant to take advantage of all the space that the widget can use in order to maximize the information it can display. -
Adaptiveness To suppo rt multiple sizes the fi rst thing
to do is de fi ne the sizes that our widget will have to suppo rt . object RandomGameWidgetSize { val COMPACT = DpSize(width = 100.dp, height = 100.dp) val MEDIUM = DpSize(width = 300.dp, height = 200.dp) val EXPANDED = DpSize(width = 300.dp, height = 300.dp) } -
Adaptiveness Then we have to make an implementation for each
of the suppo rt ed sizes: RandomGameWidgetContentCompact.kt RandomGameWidgetContentMedium.kt RandomGameWidgetContentExpanded.kt -
Adaptiveness To inform Glance about the de fi ned dimensions,
we must set the sizeMode of our widget to SizeMode.Responsive, providing to it the dimensions to be suppo rt ed. private class RandomGameWidget : GlanceAppWidget() { override val sizeMode: SizeMode = SizeMode.Responsive( sizes = setOf( RandomGameWidgetSize.COMPACT, RandomGameWidgetSize.MEDIUM, RandomGameWidgetSize.EXPANDED ) ) ... } -
Adaptiveness Source: h tt ps://developer.android.com/develop/ui/compose/glance/build-ui 150 dp 150 dp DpSize(
width = 100.dp, height = 100.dp )// COMPACT DpSize( width = 300.dp, height = 200.dp )//MEDIUM DpSize( width = 300.dp, height = 300.dp )//EXPANDED LocalSize.current “For each de fi ned size, the content is created and mapped to the speci fi c size when the AppWidget is created or updated. The system then selects the best fi tt ing one based on the available size.” -
Adaptiveness “For each de fi ned size, the content is
created and mapped to the speci fi c size when the AppWidget is created or updated. The system then selects the best fi tt ing one based on the available size.” private class RandomGameWidget : GlanceAppWidget() { override val sizeMode: SizeMode = SizeMode.Responsive(…) … override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { when (LocalSize.current) { RandomGameWidgetSize.COMPACT -> RandomGameWidgetContentCompact(game) RandomGameWidgetSize.MEDIUM -> RandomGameWidgetContentMedium(game) RandomGameWidgetSize.EXPANDED -> RandomGameWidgetContentExpanded(game) } } } } Source: h tt ps://developer.android.com/develop/ui/compose/glance/build-ui -
Random Game Widget goals For our widget we would like
to achieve these goals: 1) It must be well integrated with the system theme. 2) Display game covers and summaries. 3) Take advantage of the space it is given to it. 4) Open the app when touched. ✅ ✅ ✅ -
Handle user interactions Glance simpli fi es handling user interaction
via the Action classes. Glance’s Action classes de fi ne the actions a user can take, and you can specify the operation pe rf ormed in response to the action. It is possible to apply an Action to any component using the GlanceModi fi er.clickable modi fi er. There are 4 of them: • actionSta rt Activity • actionSta rt Service • actionSendBroadcast • actionRunCallback Source: h tt ps://developer.android.com/develop/ui/compose/glance/user-interaction -
Handle user interactions In our case we’ll use actionStartActivity to
launch the GameDB app in a speci fi c page through deep link handling. Source: h tt ps://developer.android.com/develop/ui/compose/glance/user-interaction private fun buildOpenAppIntent(gameId: Int) = Intent(Intent.ACTION_VIEW).apply { data = "app://gamedb.fabiocati.it/$gameId".toUri() } override suspend fun provideGlance(context: Context, id: GlanceId) { ... val deepLinkIntent = buildOpenAppIntent(game.id.toInt()) provideContent { Scaffold( modifier = GlanceModifier.clickable(actionStartActivity(deepLinkIntent)) ) { ... } } ... } -
Random Game Widget goals For our widget we would like
to achieve these goals: 1) It must be well integrated with the system theme. 2) Display game covers and summaries. 3) Take advantage of the space it is given to it. 4) Open the app when touched. ✅ ✅ ✅ ✅ -
Conclusions • Widgets are not a single entity but more
a group of them • The core component of a widget is the AppWidgetProvider which extends BroadcastReceiver. • Remote views is a (serializable) class that describes UI in widgets. • Because of it, making a widget was HARD • Jetpack Glance comes to rescue giving us the same Compose APIs to build widgets. -
Additional resources • Widget Quality Tiers: h tt ps://developer.android.com/design/ui/mobile/guides/widgets/widget_quality_guide •
Canonical Widget Layouts: h tt ps://developer.android.com/design/ui/mobile/guides/widgets/layouts • Spotlight Week: Design and Develop Widgets: h tt ps://android-developers.googleblog.com/2025/03/spotlight-week-widgets.html