Demystifying Jetpack Glance for app widgets
We recently announced the first Alpha version of Glance, initially with support for AppWidgets and now for Tiles for Wear OS. This new framework is built on top of the Jetpack Compose runtime and designed to make it faster and easier to build “glanceables” such as app widgets without having to handle a lot of boilerplate code or lifecycle events to connect different components.
However, it’s important to understand that Glance APIs are not interoperable with Jetpack Compose and also have certain limitations. In this post we’ll demystify some of these limitations and highlight the key points to consider when building AppWidgets with Glance.
The content is divided into the following topics:
- Declaring app widgets
- Theming & styles
- Updating, recomposition and side-effects
- Android Studio tooling
Important: this blog was written for the Glance-alpha03 version. Future versions might obsolete some of the content below.
Declaring app widgets
Glance’s main goal is to simplify the UI creation for app widgets and connect the different parts so you can focus on building beautiful app widgets. We are working on streamlining certain tasks, be aware that there are some limitations and that manual work is still required.
“Why do I need the GlanceAppWidgetReceiver?”
Android OS uses BroadcastReceivers to notify app widgets of “lifecycle” events, for instance when it is enabled the first time or when it needs to be updated. To simplify the API, the Android SDK provides the AppWidgetProvider class. Glance goes one step further with the GlanceAppWidgetReceiver class, by wiring the receiver and the GlanceAppWidget instance, managing the different events, and refreshing its content when necessary.
class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget = MyAppWidget()}
Note: There may be use cases for capturing app widget events such as onEnabled, but it’s not required for building the UI.
“Do I still need to define the metadata in XML?”
Currently, yes. We are looking into ways to simplify this step, but for the time being, follow the official app widgets guidance.
“Do I need to provide the android:previewLayout or the android:previewImage?”
Yes, otherwise the app widget picker in the launcher won’t be able to display a preview, making it hard for users to understand your app widget functionality.
Do you need both? While we recommend providing both for a better experience, previewLayout is only used in Android 12 and higher and does require you to redefine the app widget UI in XML (again, we are looking into ways to simplify this).
Theming & styles
This section covers some common points of confusion around theming for AppWidgets and Glance.
“Can I use the Jetpack Compose MaterialTheme composable?”
No, we don’t recommend mixing Jetpack composables with Glance. Wrapping your Glance composables with the MaterialTheme (or any sort of “Compose theme”) won’t have any effect on the Glance components. (See some of the limitations below.)
As with some of the other features described here, we’re working on providing an API to simplify this. Until then, keep your styles, colors, and attributes in a single Kotlin object and apply them manually to your composables.
“Should I provide a Color value or a Color resource Id?”
If you want to take advantage of “Dynamic Colors” or support for light and dark themes, you should use the color resource ID. Otherwise, the app widget won’t dynamically change its color (for example, when toggling between light and dark modes) because the color value was already resolved on the first placement of the widget.
Box(modifier = modifier
.appWidgetBackground()
.background(R.color.m3_sys_color_dynamic_dark_background)
) { … }
Note: We are using the color resources provided by the Material Components library. To handle day or night themes or backward compatible devices, wrap the color into another ID and redefine it in each folder (i.e: values/colors.xml, values-v31/colors.xml, …)
“Can I provide custom fonts?”
No, limitations with app widgets prevents Glance from displaying custom fonts. Check the TextStyle class to view available fonts and styles.
“How do I create shapes like the ones in Jetpack Compose?”
Glance for AppWidgets translates composable code into actual RemoteViews + XML. This means you don’t have a “free canvas” like in Jetpack Compose. The same limitations that apply for RemoteViews also apply to Glance.
The best way to achieve shapes (such as rounded corners) is to fall back to drawables + XML:
<shape android:shape=”rectangle”> <corners
android:radius=”@dimen/app_widget_background_corner_radius” /> <solid android:color=”@color/color_background” /></shape>
and pass the drawable ID to the composable:
Box(modifier = modifier.background(
ImageProvider(R.drawable.rounded_corner_background)
))
Updating the app widget
Okay, you’ve built your glanceable. But how do you update, load the data to display, or handle data/state changes? 👇
“Do I need the receiver to update the app widget?”
The receiver is needed to handle Android OS events such as “onUpdate,” but you can update your GlanceAppWidget instance from anywhere in your app as long as you do it inside a coroutine. Avoid sending update events to the receiver, and instead use one of the following mechanisms:
// Update a specific instance of MyAppWidget.
MyAppWidget().update(context, glanceId)// Update all placed instances of MyAppWidget.
MyAppWidget().updateAll(context)// Iterate over all placed instances of MyAppWidget
// and update if the state of the instance matches
// the given predicate.
MyAppWidget().updateIf<Preferences>(context) { state ->
state[KEY_TYPE] == TYPE_DESTINATION
}
Note: Use the GlanceAppWidgetManager.getGlanceIds(..) method to retrieve the GlanceIds.
Also, we recommend checking the “Optimizations for updating widget content” guide, but instead of using the AppWidgetManager use one of the mechanisms in this section.
“How do I update the widget from the configuration activity?”
android:configure allows you to provide an activity that will be launched when the widget is first placed (or on reconfiguration). As explained in this guide, you can retrieve the appWidgetId from the Intent. Currently there is no mechanism to convert it into a GlanceId. While we are looking into it, you could use the last GlanceId from GlanceAppWidgetManager.getGlanceIds(..) since it will be the last placed appWidgetId.
“Can I update my app widget from a non-main thread/scope?”
Yes, calling any of the update methods from a different thread/scope it’s allowed and encouraged, but that does not mean you can execute long-tasks inside the Content() function.
“Can I use side-effects?”
Because recomposition for Glance is currently not supported and most of the side-effects rely on it, we don’t recommend using side-effects. Even though they’re common in Jetpack Compose, they are not supported in Glance because app widgets are not bound to a lifecycle (such as an Activity), and the process might be killed at any point.
“Can I use remember?”
remember is the mechanism in Jetpack Compose for managing state, by storing values in the composition. In Glance, that’s not possible since the composition is destroyed every time the widget gets updated. Instead, define a GlanceStateDefinition inside your GlanceAppWidget, to ensure your app widget state is persisted, even if the process gets killed.
Here is a quick example:
// Define the state definition in your app widget…
override val stateDefinition = PreferencesGlanceStateDefinition// … inside a composable.
val counter: Int = currentState(CounterKey) ?: 0// … somewhere outside of the composition (e.g ActionCallback).
updateAppWidgetState(context, glanceId) { state ->
state[CounterKey] = (state[CounterKey] ?: 0) + 1
}// Don’t forget to trigger the update. :)
MyAppWidget().update(context, glanceId)
Note: updateAppWidgetState doesn’t automatically trigger the app widget update. You must manually trigger it.
“How do I fetch data?”
We are working on ways to make this easier, but this is not specific to Glance. It’s the same for app widgets development with RemoteViews.
The best way to fetch data asynchronously from the background is by using WorkManager and storing the data as recommended in the Guide to background work. The important part is to avoid launching new workers either consecutively (for example, inside GlanceAppWidgetReceiver.onUpdate) or when data is already loaded.
Android Studio tooling
We are working on providing better tools to speed up Glance development.
“Can I use @Preview for Glance”
Not at the moment. Given the way Glance works by translating Composables into actual RemoteViews and XML, the preview panel provided by Android Studio won’t work for Glance composables out of the box. We are looking into ways to make this possible.
“How can I apply changes faster?”
To speed up development, we recommend creating the following launch configuration. This ensures the app widget is updated without launching the activity or having to remove the app widget and place it again.
- In Android Studio, choose Run > Edit Configurations…
- Create a new configuration:
- Check the Always install with package manager checkbox.
- Under Launch Options, select Nothing from the Launch menu.
What’s next?
We hope this post has provided some clarity to some of the common issues you may experience when starting with Glance. Here are some additional resources:
- Try Glance and provide your feedback!
- Follow Glance releases
- Check out the demos
- The official sample in GitHub
- AndroidX repo demos - Join the community
- Stackoverflow #glance, #glance-appwidget
- Join Kotlin Slack group (channel #glance)
Do you need help building a widget?
Our experts can help you https://pibi.studio/tasks