Building a Compose Widget Using Jetpack Glance

Stay hydrated

Matthew Meehan
Better Programming

--

Photo by Manki Kim on Unsplash

Recently, Android has announced the Alpha release for Jetpack Glance, a widget-building framework built on top of Jetpack Compose.

With the release of Android 12, app widgets have received a revamp to drastically improve both developer and user experience.

As such, Glance provides a convenient, streamlined way to create widgets with far less code. The framework provides a base set of composables that can be leveraged to create beautiful and functional user experiences.

As an introduction to utilizing Glance, a simple app widget will be designed to keep track of how many glasses of water a user has drank in a day. The Github repository can be found here.

Water Widget

Initial Setup

To begin creating the app widget, some preliminary steps will need to be performed.

Glance is compatible with the latest stable version of Android Studio, so it will need to be updated accordingly.

As for the project, several gradle dependencies will be required.

Glance and Compose Dependencies

Since Glance is based on the compose framework, several compose dependencies are required along with Glance itself. Additionally, compose will need to be enabled for the project.

Enabling Compose

Since Glance is only available as a SNAPSHOT, which are builds periodically generated in the androidx.dev repository, this will need to be included in settings.gradle:

Including the Snapshots Repository

Declaring and Previewing the Widget

Prior to writing any composables, the widget must be declared in the android manifest with additional defining information.

Widget Info

Glance widgets, just like non-composable widgets, require an xml definition for their attributes.

In the project, a new resource directory can be created named xml, which will contain it. Within xml a new file, water_widget_info.xml will be made.

Widget Information

The appwidget-provider provides android with attributes about the widget.

  • The widget name (In this case WaterWidget)
  • Its minimum sizing information in DP
  • How the widget can be resized by the user
  • The default size of the widget, which is new in Android 12, this is automatically set as 2x2 but can be scaled from 2x1 to 4x3
  • The category, which defines the type of widget; this can be Home Screen, KeyGuard, or Search Box
Widget Resizing in Android 12 Demo

With this basic information, this xml info can now be included in the manifest.

Android Manifest

To properly declare the widget, a receiver will need to be written in the Android Manifest.

As a preliminary step, some boilerplate code will be written to create receiver and widget classes.

First, a file will be created called WaterWidget.kt, which will house both of these components.

Water Widget and Receiver

The WaterWidgetReceiver extends the GlanceAppWidgetReceiver which acts as a provider for the widget.

This provided widget, WaterWidget , contains a composable function Content() that will contain the widget layout.

Once these classes are defined, the receiver can be added to the manifest.

Manifest Receiver for Water Widget
  • name will utilize the previously created receiver class: WaterWidgetReceiver
  • The intent filter is declared to receive updates to the widget
  • The metadata will use the previously defined attributes in water_widget_info.xml

Initial and Preview Layout

As a final step to this initial process, an initial and preview layout will need to be defined in order for the widget to function properly.

Unfortunately, these layouts will need to be written in xml, as to compose is not yet supported here. Since this part of the code is still based on RemoteView, one of the supported xml layouts will be required:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

ConstraintLayout will not work for the widget’s initial or preview layout.

The initial layout is a legacy item that contained the layout for a widget declared in xml.

For now, the documentation for Glance explains that an initial layout is required, but this will be removed in the future. As such, a simple frame layout will act as a placeholder until this can be removed with upcoming updates to Glance.

Initial Layout

The preview layout will display a preview of the widget when the user selects it from the widgets menu and drags it onto the home screen.

Preview Layout

This preview is built to mimic what the widget will look like when built in Compose. These can be added to the attributes in water_widget_info.xml:

Preview and Initial Layouts added to Widget Information

Note: Prior to Android 12, previews use the android:previewImage attribute and use an image instead of xml to display the preview. For backward compatibility use this method as well.

Hopefully, as Glance is updated, it will remove the need to create any of these xml components, as it is counterintuitive to create both an xml and a compose layout.

Creating the Widget Layout

With all of this in place, it’s time to write some composables for the widget. The widget itself is simple, it will contain four major components:

  • Text showing a counter for how many glasses of water the user has drank
  • Text showing the daily recommended number of glasses
  • A button for adding a glass to the counter
  • A button for resetting the counter

Writing Composable Content

A separate file can be created called WaterWidgetContent.kt, which is where the composable functions will reside.

Note: since these are Glance composables, the components used will be from the Glance framework, not from standard compose. As such, they will use GlanceModifier rather than Modifier to modify their behavior.

Starting with the counter text, this function will retrieve a formatted string and pass it the number of glasses drank as an argument.

Water Widget Counter

This formatted string takes a number type (in this case an Int), to display the user’s progress. Notice these Composables are provided by Glance, not by the usual Androidx library. These UI components and the Glance Modifier behave very similarly to their Androidx counterparts.

Next, the goal text will contain logic to choose a string depending on whether the user has reached the goal or not.

Water Widget Goal

The constant RECOMMENDED_DAILY_GLASSES can be defined back in WaterWidget.kt in a companion object.

Recommended Daily Glasses

Now, the buttons for adding to and resetting the counter can be declared. First, vector assets for each of the images will need to be added in the drawable res package.

Plus Vector Asset
Garbage Vector Asset

For adding to the counter a plus asset will be used, while the counter reset will use a “garbage can” asset. These images can be declared inside a Row to align them horizontally.

Widget Button Layout

In order to write the on-click handling, a Glance ActionCallback must be created for each of the two buttons.

ActionCallbacks

The Glance ActionCallbacks are exactly what they imply; they perform an action when notified.

In a new file called WaterWidgetActions, two classes can be created which will extend ActionCallbacks .

ActionCallbacks

These functions make use of the suspend function of the parent class onRun(); when the callback is invoked, the logic in onRun() will be performed.

These callbacks can be attached to their corresponding image buttons using actionRunCallback<T>() in conjunction with clickable(…) .

Buttons with Clickable Modifiers

However, before implementing the actual logic in either of these callbacks, state handling must be examined.

Using State in Glance

For the widget to properly keep track of how many times the user has incremented the counter, it will need to keep track of a state; this can be done with components provided by Glance.

Back in the AddWaterClickAction , the onRun() function can be modified:

Add Water Logic

Here, the suspend function updateAppWidgetState(…) provides a scope to access and edit a data store.

In this case, since PreferencesGlanceStateDefinition was passed as a parameter, android’s DataStore is used. Values from DataStore can be retrieved using an appropriate Preferences.Key depending on the expected return value type. In this case (for the number of glasses of water), an intPreferencesKey is used.

Once retrieved, the function compares the stored number of glasses to a defined upper bound (999 was used in this case just to prevent the user from incrementing the counter limitlessly), and adds one to the value if it does not exceed the boundaries. Once this operation is performed, WaterWidget().update(context, glanceId) is called to update the widget state with new data.

The constants for WATER_WIDGET_PREFS_KEY and MAX_GLASSES can be defined in the companion object for the WaterWidget class:

Constants for WaterWidget

For the ClearWaterClickAction the logic is simpler:

Clear Water Logic

Similar to the other callback, this one accesses the same stored value for number of glasses. However, this function sets the stored value to zero, resetting the users daily progress.

With these values properly stored, they can be accessed from the state within the widget itself.

Putting it all Together

In WaterWidgetContent.kt a composable function can be created to encapsulate each of the individual components of the widget, as well as expose the context and the state to each of them.

Water Widget Content

Here, Glance uses LocalContext.current to provide the widget context to each composable, while stateDefinition is overriden to provide the state. Since DataStore was used previously to store the values in the callbacks, PreferencesGlanceStateDefinition is used again to access them. Now, currentState<Preferences>() can be used within the composable function to retrieve the necessary data. Just like before, the number of glasses a user has drank today can be retrieved using the WATER_WIDGET_PREFS_KEY and passed to each of the text components.

As mentioned before, the GlanceAppWidget contains a composable Content() function, which is where this composable function can be called. With the layout complete, the functioning widget can be added to the home screen!

Widget including Content
Adding the Widget to Home Screen

As Glance is still only in its early stages, this code will be subject to change, but hopefully, this early look can help with what is to come!

--

--