Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Your app at a Glance: App Widgets in Compose

Avatar for Fabio Catinella Fabio Catinella
April 26, 2025
50

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.

Avatar for Fabio Catinella

Fabio Catinella

April 26, 2025
Tweet

Transcript

  1. Summary • What is a widget? • How does it

    work? • Making one of it was HARD • Widget and Jetpack Compose • New widget creation • Conclusions
  2. 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
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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>
  9. 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>
  10. 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>
  11. 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" />
  12. 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)
  13. 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
  14. 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
  15. 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…
  16. 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
  17. 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”
  18. 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.”
  19. 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, ) … }
  20. 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") } } }
  21. 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" ) }
  22. 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" ) }
  23. 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.
  24. 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.
  25. 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.
  26. 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" ) } }
  27. 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. ✅
  28. 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.
  29. 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
  30. 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) } } ... }
  31. 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 ) ) } … }
  32. 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. 🥲
  33. Adding images However, It is still possible to create a

    custom component that uses Coil at its core and allows us to load images.
  34. 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) ) } }
  35. 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 { ... } }
  36. 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. ✅ ✅
  37. 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.
  38. 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) }
  39. Adaptiveness Then we have to make an implementation for each

    of the suppo rt ed sizes: RandomGameWidgetContentCompact.kt RandomGameWidgetContentMedium.kt RandomGameWidgetContentExpanded.kt
  40. 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 ) ) ... }
  41. 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.”
  42. 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
  43. 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. ✅ ✅ ✅
  44. 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
  45. 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)) ) { ... } } ... }
  46. 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. ✅ ✅ ✅ ✅
  47. 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.
  48. 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