Exploring Jetpack Compose for Widgets with Glance

Widgets on Android devices provide users with a way to access core pieces of information and functionality directly from the home screen of their device. As we continue to look for ways to improve our users experiences with our product and surface important information to them from outside of the app, widgets have been a great way for us to achieve this. With the availability of Jetpack Glance, we’re now able to create widgets using Composables – and if you’ve had to create widgets using RemoteViews, then you know how much easier this is going to be!

I recently worked on a new Widget for the Buffer Android app for a new feature we launched. Our streaks feature tracks the weekly posting streak for your account, helping you to keep track of how consistently you post on your social channels.

As part of this feature, we felt that a widget would help to raise awareness of your streak directly from your device home screen. The streak itself is quite simple, which provides a great opportunity to share how to build widgets with Glance, without getting caught up too much in the UI details of Compose. Things are a little different from how we might usually work with Compose and in this blog post I want to share those details with you.


Widget Hierarchy

Before we get started with building a widget, it’s important to understand the hierarchy that makes up the widget. We won’t dive into the architecture of things under the hood, but it’s helpful to look at this at a higher level to better understand what we are building.

When it comes to widgets with Glance, we have 4 main components that we need to think about:

  • App Manifest – we use the manifest file to declare our widget and the configuration for it
  • GlanceAppWidgetReceiver – this is the receiver which provides the instance of our GlanceAppWidget, along with receive any updates to refresh the contents of our widget
  • GlanceAppWidget – this is the class that provides the composition of our widget
  • Glance UI – this is the composable that is used to compose the UI for our widget

As we work through this post, we’ll see how we need to work with these different components to construct and provide our Glance widget.


Adding Dependencies

Before we can get started, we need to add some dependencies to our project. For Glance there are a collection of dependencies available, and what you add for your project will depend on your needs. You’ll at least need to add the glance-appwidget and one of the material dependencies, with the preview dependencies being recommended as they’ll be helpful for creating previews of your widgets for viewing within the IDE.

dependencies {
    implementation "androidx.glance:glance-appwidget:1.1.1"

    implementation "androidx.glance:glance-material3:1.1.1"

    implementation "androidx.glance:glance-material:1.1.1"

    implementation "androidx.glance:glance-preview:1.1.1"

    imlementation "androidx.glance:glance-appwidget-preview:1.1.1"
}

Creating the Widget

Now that we have our dependencies in place, we’re ready to start building our widget. To create a widget that can be added to a users device, we need to provide an instance of the GlanceAppWidget class that will be used to compose the Glance widget.

class StreaksWidget : GlanceAppWidget() {


}

With this class now in place we’re going to need to override the provideGlance function, this is what is going to be used to compose the widget UI. Within this function we’ll utilise the provideContent function which is where we will need to provide the composition for our UI – you can think of this as the setContent function that we use for composing UI inside of an activity class.

override suspend fun provideGlance(context: Context, id: GlanceId) {
    provideContent {

    }
}

Inside of this block we’re now going to compose the UI for our widget, for which we’ll start by using the GlanceTheme function. This is similar to the MaterialTheme wrapper that we use in Jetpack Compose, but specifically built for Glance. We’re not going to be doing any customization here, but you can read more about this functionality if you need to override the default theme.

provideContent {
    GlanceTheme {
                    
    }
}

At this point, we’re now ready to compose the UI for our widget. As we previously saw, we’re creating a pretty simply UI for our widget – consisting of an image with some text.

When it comes to composing this UI, we’re not going to dive into the specifics of building UI with compose as I want to focus on Glance itself, so we’ll be skipping over some details of the UI to simplify things here.

For the main body of our widget UI, we’re going to see something like the following:

import androidx.glance.layout.Column
import androidx.glance.layout.Spacer
import androidx.glance.layout.height

@Composable
fun Streak(
    modifier: GlanceModifier = GlanceModifier,
    content: WeeklyPostingStreak
) {
    Column(...) {
        Image(...)
        Spacer(modifier = GlanceModifier.height(12.dp))
        Text(...)
    }
}

This is quite simply and is likely something similar to what you’ve build previously with Jetpack Compose. The main difference here is that the composables we are using are from the androidx.glance package and not the standard foundation package that we access for composables. This is because Glance widgets are restricted to the limitations of AppWidgets and RemoteViews, so we see a reduced set of composables available for building widgets with Glance. For example when it comes to layouts, we can use the Scaffold, Box, Row and Column composables and we still have composables such as the Image, Text and Spacer. However, when it comes to using this you may noticed reduce functionality, such as specific modifiers that you are used to using. For simpler widget UIs you should be fine in most cases, but there may be a few situations where you have to think slightly differently. You can read more here about what components are available.

With our Streak widget in place, we’re not going to create a StreakWidgetContnet composable that will wrap this in a Box and display it over a background, allowing our widget to work in a responsive way.

import androidx.glance.layout.Box
import androidx.glance.layout.wrapContentSize
import androidx.glance.layout.padding

@Composable
fun StreakWidgetContent(
    modifier: GlanceModifier = GlanceModifier,
    content: WeeklyPostingStreak
) {
    Box(
        modifier = modifier.background(...),
        contentAlignment = Alignment.Center
    ) {
        Streak(
            modifier = GlanceModifier.wrapContentSize().padding(16.dp),
            content = content
        )
    }
}

Now that we have the UI of our widget in place, we can go ahead an compose it with the the GlanceTheme that we defined in a previous step.

provideContent {
    GlanceTheme {
        StreakContent(content = ...)
    }
}

Dependency Injection

One thing you may have noticed above is that we don’t have the data available to pass the required data to our widget composable. To get this data, we need access to the GetWeeklyPostingStreak use case class that will fetch this information from the network/cache. However, that class relies on dependency injection for its dependencies, so we’re going to need to find a way to inject this use case into our widget. There are two ways of doing this and we’re going to cover the first while we are in the code for the widget.

In my app I am using Hilt and there isn’t an officially supported way of accessing dependencies inside of widgets yet, so we’re going to workaround this for now. We’ll start here by defining a new EntryPoint interface for our widget that defines the dependency that we need to access, just like we would for other entry points in our app.

@EntryPoint
@InstallIn(SingletonComponent::class)
interface StreaksWidgetEntryPoint {
    fun getWeeklyPostingStreak(): GetWeeklyPostingStreak
}

Next, we’re going to go ahead an manually create the reference to the EntryPoint within our code For this we need to use the EntryPointAccessors along with a reference to the ApplicationContext, which will give us an instance of our EntryPoint along with the dependencies that have been declared.

override suspend fun provideGlance(context: Context, id: GlanceId) {
    try {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(context.applicationContext, StreaksWidgetEntryPoint::class.java)
    } catch (e: Exception) { }
}

Using this we can now execute our use case and store a reference to the result of the operation.

override suspend fun provideGlance(context: Context, id: GlanceId) {
    var data: WeeklyPostingStreak? = null
    try {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(context.applicationContext, StreaksWidgetEntryPoint::class.java)
        hiltEntryPoint.getWeeklyPostingStreak().run()?.let { data = it }
    } catch (e: Exception) { }
}

Now that we have this data available, we can pass it to our composable to satisfy the required data that we were previously missing.

data?.let { streakData ->
    StreakContent(content = streakData)
}

You may notice here though that the data we retrieve is nullable – this could be from an exception being thrown, or the data not being available from the network/cache. In these cases we still need to show something within our widget to handle this error case, so here we’ll simply display an error message to let the user know that we were unable to load the content.

data?.let { streakData ->
    StreakContent(content = streakData)
} ?: run { Text(text = "Unable to load Streak") }

Note: If you are looking to access string resources from within your Glance widget, then you need to utilise LocalContext.current to access the application resources.


Launching Intents

In some cases widgets will be for purely informative purposes, but in others we will want to trigger some kind of Intent when the user interacts with it. In the case of this widget, we want to link the Composer (used for creating social posts) when the widget is clicked, so for this we can use the actionStartActivity function. When it comes to a Streak, in some cases they are sharable – so for example if the user has hit their streak and they tap the widget, we pass the streak data and then the text/image for that streak are loaded into the composer, allowing the user to share their streak. In the case where the users streak is at risk, tapping the widget will open the Composer in a blank state so that they can create something to get their Streak back on track.

data?.let { streakData ->
    val streakModifier = GlanceModifier.clickable(
        if (streakData.canShareStreak) {
            actionStartActivity(Activities.Composer.intentForStreak(streakData))
        } else {
            actionStartActivity(Activities.Composer.intent())
        }
    )
    StreakContent(modifier = streakModifier, content = streakData)
} ?: run { Text(text = "Unable to load Streak") 

Registering the Widget

At this point we’ve created our widget and enabled it to be interacted with by the user, we now just need to register it with the system. We need to start here by creating a new instance of the GlanceAppWidgetReceiver class, which is used to manage the lifecycle of the widget via broadcast recievers.

class StreaksWidgetReceiver : GlanceAppWidgetReceiver() {

    override val glanceAppWidget: GlanceAppWidget = StreaksWidget()

}

With this class created, we’ll need to go ahead and declare a reference to this inside of our application manifest, declaring the APPWIDGET_UPDATE intent-filter action that that our widgets can be updated as required.

<receiver android:name=".streaks.StreaksWidgetReceiver"
    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/streaks_widget_info" />
</receiver>

Next, we’ll go ahead and create a new metadata file for our widget, which allows us to declare some configuration properties for our widget. These values will depend on the use case for your widget, but for Glance you’ll need to at least provide the initialLayout value for what should be displayed before your widget has been loaded.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/glance_default_loading_layout"
    android:initialLayout="@layout/glance_default_loading_layout"
    android:minWidth="140dp"
    android:minHeight="90dp"
    android:previewImage="@drawable/streaks"
    android:resizeMode="none"
    android:widgetCategory="home_screen" />

The GlanceAppWidgetReceiver instance that we create here can have the Hilt AndroidEntryPoint annotation applied to it, allowing you to perform injection into your receiver to then be passed to the widget. However, these are not suspending functions, so you will need to ensure that you correctly manage the scopes to ensure threading is correctly handled.


Wrapping Up

Throughout this post we’ve learn how we can create app widgets using Glance and Jetpack Compose. This approach has provided us with an easier way to create app widgets, giving users access to core parts of our app directly from their home screens (and beyond!). If you’re looking to learn more about how to create widget with Glance then you can read more from the official guides here.