Kickstart Your Widget Adventure: An Essential Guide to Android App Widgets with Jetpack Glance
The App widget has been with us since Android 1, and there were no major updates until Android 12 when the Google team announced a revamp of this feature, making it more useful, beautiful, and discoverable.
They have also introduced a new framework called Jetpack Glance with the main goal of simplifying UI creation for app widgets and connecting different parts, allowing us to focus on building beautiful app widgets. With Jetpack Glance, we can build UI using the same syntax as Jetpack Compose, which we are already familiar with.
By creating an App widget, you provide ‘at-a-glance’ access to your apps on the user’s home screen, which can improve user engagement and retention.
In this article, I will cover the basics of how to create an App widget.
- Start by creating a very simple app widget using XML to understand how App Widgets work.
- Implement a basic version of the Glance Widget.
- Build the Random Quote Widget using Glance.
App widgets are miniature app views that you can be embed in other apps — such as the home screen — and receive periodic updates.
The behavior of an app widget is published by an “app widget provider.” Android provides the AppWidgetProvider
class, which extends BroadcastReceiver
, as a convenience class to define the app widget behavior and aid in handling the broadcasts.
Meanwhile, AppWidgetProviderInfo
describes the meta data for an installed AppWidget provider. And theAppWidgetManager
class to updates AppWidget state; gets information about installed AppWidget providers and other AppWidget related state.
A traditional way to create an App widget
To create an App widget, we need to create a class that is implements the AppWidgetProvider
class. As mentioned earlier, AppWidgetProvider
extends from BroadcastReceiver
. However, it only receives event broadcasts that are relevant to the widget, such as when the widget is updated, deleted, enabled, and disabled.
class ExampleAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
Next is we need to declare an AppWidgetProviderInfo
to describe the metadata for the widget. We need to store the file in the project’s res/xml/
folder.
<!-- res/xml/example_app_widget_provider_info.xml -->
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="2"
android:targetCellHeight="1"
android:maxResizeWidth="110dp"
android:maxResizeHeight="40dp"
android:updatePeriodMillis="86400000"
android:description="@string/example_appwidget_description"
android:previewLayout="@layout/example_appwidget_preview"
android:initialLayout="@layout/example_loading_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>
To note, there is a major update in Android 12 where several metadata are only available from Android 12 and above, including:
targetCellWidth
andtargetCellHeight
; Starting in Android 12, these attributes specify the default size of the widget in terms of grid cells. These attributes are ignored in Android 11 and lower, and can be ignored if the home screen doesn't support a grid-based layout.maxResizeWidth
andmaxResizeHeight
; Specify the widget’s recommended maximum size. If the values aren’t a multiple of the grid cell dimensions, they are rounded up to the nearest cell size. ThemaxResizeWidth
attribute is ignored if it is smaller thanminWidth
or if horizontal resizing isn't enabled. SeeresizeMode
. Likewise, themaxResizeHeight
attribute is ignored if it is greater thanminHeight
or if vertical resizing isn't enabled.description
; Specifies the description for the widget picker to display for your widgetpreviewLayout
; Specifies a scalable preview, which you provide as an XML layout set to the widget’s default size. Ideally, the layout XML specified as this attribute is the same layout XML as the actual widget with realistic default values. They recommend specifying both thepreviewImage
andpreviewLayout
attributes so that our app can fall back to usingpreviewImage
if the user's device doesn't supportpreviewLayout
. (See also: Backward compatibility with scalable widget previews)
widgetFeatures
; Declares features supported by the widget. For example, if we want your widget to use its default configuration when a user adds it, specify both theconfiguration_optional
andreconfigurable
flags. This bypasses launching the configuration activity after a user adds the widget. The user can still reconfigure the widget afterward.
Next, we need to declare the widget in our AndroidManifest.xml
, similar to how we declare a broadcast receiver. To declare a widget, we specify it within the <receiver>
tag, which receives the ACTION_APPWIDGET_UPDATE
intent and also include the metadata about the app widget that we’ve defined earlier.
<!-- app/src/main/AndroidManifest.xml -->
<application>
<receiver android:name="ExampleAppWidgetProvider"
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/example_app_widget_provider_info" />
</receiver>
</application>
Now, we need to create the UI. If you observe in our ExampleAppWidgetProvider
class, there is a function updateAppWidget(context, appWidgetManager, appWidgetId)
, and this is where we can build the UI.
To build the UI for the widget, we can’t use the same View
as we usually do when we creating an app. Instead, we need to create it using a RemoteViews
.
RemoteViews
is a class that describes a hierarchy of views that can be displayed in another process, in this case is the home screen. Another example of using RemoteViews
is for custom notifications. To note, since it can be displayed in another process, there are several limitations on what we can draw inside a RemoteViews
(See also: RemoteViews | Android Developers)
class ExampleAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
...
}
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val widgetText = context.getString(R.string.appwidget_text)
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.example_app_widget_provider)
views.setTextViewText(R.id.appwidget_text, widgetText)
...
}
In this example, we are only adding a TextView
in our RemoteViews
. Try to compiling and running the app.
Tip! Android Studio can automatically generate a set of AppWidgetProviderInfo
, AppWidgetProvider
, and view layout files. Simply choose “New > Widget > App Widget.
” and voila! You now have an app widget.
Now that you know how to create an app widget, let’s dive deeper into how to create one with Jetpack Glance! 🙌
Basic version of the Glance Widget
But first, how Jetpack Glance works?
Basically, Jetpack Glance is a framework built on top of the Jetpack Compose runtime that lets us develop and design app widgets using Kotlin APIs.
Glance offers a base set of Composables to assist in building “glanceable” experiences. The Google team has mentioned that in the future, Glance will not only support widgets but will also serve as the foundation for other Glance components.
Glance can translate Composables into actual RemoteViews and display them in an app widget using the Jetpack Compose runtime. This is why Glance requires Compose to be enabled and depends on Runtime, Graphics, and Unit UI Compose layers, but it’s not directly interoperable with other existing Jetpack Compose UI elements.
If your app is not configured to use Compose, follow the steps in Set up Compose for an existing app. If all good, the next step is to add Glance dependencies to your app’s module Gradle file:
dependencies {
// For Glance support
implementation("androidx.glance:glance:1.0.0")
// For AppWidgets support
implementation "androidx.glance:glance-appwidget:1.0.0"
// For interop APIs with Material 2
implementation "androidx.glance:glance-material:1.0.0"
// For interop APIs with Material 3
implementation "androidx.glance:glance-material3:1.0.0"
}
You don’t need to include all the dependencies; only add the ones that you truly require.
The next step is to create a class that extends from GlanceAppWidgetReceiver
.
class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
// Let MyAppWidgetReceiver know which GlanceAppWidget to use
override val glanceAppWidget: GlanceAppWidget = // TODO ("put MyAppWidget() here")
}
So, what is this?
In the traditional way, we have the AppWidgetProvider
class. The GlanceAppWidgetReceiver
class, essentially an AppWidgetProvider
using the provided GlanceAppWidget
instance, wires the receiver and the widget to handle various events, and refreshing its content when necessary.
Next, we need to create a GlanceAppWidget
, which is an object responsible for handling the composition and communication with AppWidgetManager
.
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
class MyAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
Box(
modifier = GlanceModifier.fillMaxSize()
.background(GlanceTheme.colors.background)
.appWidgetBackground(),
contentAlignment = Alignment.Center,
) {
Text(
text = "Glance Widget!",
modifier = GlanceModifier.padding(16.dp),
style = TextStyle(
fontSize = 24.sp,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold
)
)
}
}
}
}
}
The UI is defined by calling provideContent
from within provideGlance
. When the widget is requested, the composition is run and translated into a RemoteViews
which is then sent to the AppWidgetManager
. Also, to note provideGlance
runs on the main thread. To perform any long running operations in provideGlance
, switch to another thread using withContext
. (See Use coroutines for main-safety for more details on how to run outside of the main thread.)
This is also crucial to note: Glance provides a modern approach to build app widgets using Compose, but is restricted by the limitations of AppWidgets
and RemoteViews
. Therefore, Glance uses different composables from the Jetpack Compose UI. If you observe, instead of using import androidx.compose.foundation.layout.Box
, we utilize the one from Glance, which is import androidx.glance.layout.Box
.
In the event there is a name clash with the Compose classes of the same name,
you may rename the imports per https://kotlinlang.org/docs/packages.html#imports using theas
keyword.
Or, you can also create new module specifically for widgets.
The next step is to define the metadata, and yes, we still need to define it in XML. The Google team has mentioned that they are still looking into ways to simplify this step. Let’s wait for their updates, or if you have any suggestions, perhaps you can open a discussion with them 😏.
<!-- res/xml/glance_app_widget_provider_info.xml -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/glance_app_widget_desc"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="110dp"
android:minHeight="40dp"
android:previewImage="@drawable/example_appwidget_preview"
android:previewLayout="@layout/glance_app_widget_provider"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="2"
android:targetCellHeight="1"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
Since the UI is in Compose,
- We set
android:initialLayout
as the loading state, and fortunately, Glance provides the default loading layout, which you can simply refer to with@layout/glance_default_loading_layout
. (However, feel free to use another XML layout if you prefer.) - Additionally, we still need to create an XML layout for
android:previewLayout
to serve as the preview for our widget. In this case, I created the file@layout/glance_app_widget_provider
.
Lastly, register the provider (or, receiver) in our AndroidManifest.xml
and the associated metadata file.
<application>
<receiver android:name=".MyAppWidgetReceiver"
android:label="@string/glance_appwidget_label"
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/glance_app_widget_provider_info" />
</receiver>
</application>
Now, try to compile and run our widget. Tadaaa ~ 💃
Now that we know how to create a widget with Jetpack Glance, why not take it a step further?
A Random Quote Widget using Glance
Well, what we’ll be creating next isn’t significantly different in terms of the UI.
So, I will skip the part that you should already be familiar with, which involves creating a GlanceAppWidgetReceiver
, GlanceAppWidget
, and AppWidgetProviderInfo
. However, you might be wondering where the source is coming from and how to make the quote different when we interact with the widget.
Okay, now…
Where the source is coming from?
Glance provides PreferencesGlanceStateDefinition
, a state definition that stores a widget’s state using DataStore’s Preferences. It returns a Preferences
instance when you use a PreferencesGlanceStateDefinition
. And you can then use its APIs to query the widget’s state.
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.currentState
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
class ZenQuotesWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
val preferences = currentState<Preferences>()
val currentQuote = preferences[quoteKey] ?: staticQuotes.random()
ZenQuotesWidgetUi(currentQuote)
}
}
}
/*
* credits: https://victorbrandalise.com/building-a-widget-using-jetpack-glance/
*/
private val staticQuotes = listOf(
"Life has no limitations, except the ones you make. ― Les Brown",
"Be curious about everything. Never stop learning. Never stop growing. ― Caley Alyssa",
"If you change nothing, nothing will change. ― Unknown",
"Dream big dreams. Small dreams have no magic. ― Dottie Boreyko",
"Don’t go through life, grow through life. ― Eric Butterworth"
)
private val quoteKey = stringPreferencesKey("currentQuote")
It says that when there is a quote stored, it will display the quote from the preferences, and if not, it will show a random quote from the staticQuotes
list. Now, the next question is, when do we store the quote?
If we leave this as it is, when the widget needs to update, it will simply display a random quote. However, we want the quote to change when we touch the widget. So, how do we achieve this?
How to make the quote different when we interact with the widget?
Glance also offers straightforward handling of user interactions via the Action
class, and there are several available actions, including:
actionStartActivity<MainActivity>()
; to launch an activityactionStartService<SyncService>()
; to launch a serviceactionSendBroadcast<MyReceiver>()
; to send a broadcast eventactionRunCallback<MyCustomAction>()
; to perform custom actions
We can apply an Action
to any component with the GlanceModifier.clickable
method, like:
@Composable
fun ZenQuotesWidgetUi(currentQuote: String) {
GlanceTheme {
Box(
modifier = GlanceModifier.fillMaxSize()
.background(GlanceTheme.colors.background)
.appWidgetBackground()
.clickable(actionRunCallback<RefreshQuoteAction>()), // TODO: (to create a RefreshQuoteAction)
contentAlignment = Alignment.Center,
) {
Text(
text = currentQuote,
modifier = GlanceModifier.padding(16.dp),
style = TextStyle(
fontSize = 16.sp,
fontStyle = FontStyle.Italic
)
)
}
}
}
In this widget, we will be performing a custom action called RefreshQuoteAction
. Here is what RefreshQuoteAction
looks like:
class RefreshQuoteAction : ActionCallback {
override suspend fun onAction(
context: Context,
glanceId: GlanceId,
parameters: ActionParameters
) {
updateAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) { preferences ->
preferences.toMutablePreferences().apply {
this[quoteKey] = staticQuotes.random()
}
}
ZenQuotesWidget().update(context, glanceId)
}
}
Within the action, we call updateAppWidgetState()
where we store a different random quote. Since we have now added a different quote, the widget should be updated with the new content. We perform the update by simply calling ZenQuotesWidget().update(context, glanceId)
. This also means we want to update the widget based on the glanceId
, so we only update the widget that was clicked by the user.
Finally, try compiling and running your widget; it should resemble the gif that I attached earlier.
I understand that we skipped many things, but don’t worry, you can find the full source code above.
The more I wrote this article, the more I realized that it really only covers the basic aspects of creating a widget. But, I hope it can help you start making your own widget.
And last but not least…
Last weekend, I had the opportunity to share how to create an App widget using Jetpack Glance at the DevFest Jakarta 2023 event. Here is my deck. Let me know if there is something I need to revisit, and please feel free to comment.
Thanks and see you in the next article! 👋