Widgets have been an important part of the Android system since 2008, and an important aspect of customizing the home screen. You can think of widgets as an “at a glance” view of your app, allowing users to see your app’s data and core functions without having to open the app from the home screen. However, AppWidget’s API hasn’t changed much since Android’s launch, and only one Android release from 2012 to 2021 included updates to the AppWidget API. With Android 12 comes some much-needed updates to the Widget API.

In this article, we’ll take a look at some of the Widget API updates that Android 12 has brought, as well as some useful tools to make Widget development even better. If you prefer to see this in video, check it out here.

How Widgets work

The Widget runs in a remote process called AppWidgetHost, such as the Home Screen Launcher, and because of that, its performance is somewhat limited. Let’s take a look at how widgets work.

On the front end, the application first registers AppWidgetProvider to define Widget behavior and AppWidgetProviderInfo to define metadata. AndroidManifest then references this information and lets the operating system read metadata from AndroidManifest, such as the Widget’s initial layout and default size, and provide a preview of the Widget. The provider uses linked accounts to update the layout and update the widgets. It is important to note that the number of builds applied to the Widget is limited, so the operating system updates the Widget with a broadcast event from the receiver that contains the update information, which means that the Widget is updated periodically by receiving information from the application.

API

The release of Android 12 brings with it a number of updates to the AppWidget API. This article will not cover all of them, but rather highlight a few that are useful for Widget building.

To realize the rounded

In Android 12, many key interface elements are starting to be rounded. To make the AppWidget look consistent with other system component styles, Android 12 introduces two new system parameters, system_app_widget_background_radius and system_app_widget_inner_radius, to round corners. The first parameter sets the radius of the corner of the Widget and the second parameter sets the radius of the corner of the view within the Widget. To use these parameters, simply define a drawable object with the system parameter Corner set, as shown in the code:

// res/drawable/app_widget_background.xml
<shape android:shape="rectangle">
    <corners android:radius="@android:dimen/system_app_widget_background_radius"></shape>

// res/drawable/app_widget_inner_view_background.xml
<shape android:shape="rectangle">
    <corners android:radius="@android:dimen/system_app_widget_inner_radius"></shape>
Copy the code

You then apply a drawable object to the Widget’s outer container, which in turn applies the rounded radius provided by the system parameter to the Widget background. Similarly, apply the drawable object of the internal view to the layout representing the container inside the Widget, as shown in the code:

// res/layout/widget_layout.xml
<LinearLayout
    android:background="@ drawable/app_widget_background"... >
    <LinearLayout
        android:background="@ drawable/app_widget_inner_view_background"... >
    </LinearLayout>
</LinearLayout>
Copy the code

△ Left: Rounded corner of Widget; Right: Rounded corner of inner view

You can see from the effect that the radius of the corner of the Widget’s inner container is currently smaller than the radius of the outer container, which is how the new parameter is used.

Dynamic color

As we announced earlier at Google I/O, starting with Android 12, widgets can use device theme colors, including light and dark themes, for buttons, backgrounds, and other components. This allows for smoother transitions and consistency across widgets.

We’ve added a dynamic color API that lets you directly capture and use the theme background, color, and other parameters provided on the Pixel device system to keep the Widget consistent with the style of the home screen:

// res/layout/widget_layout.xml
<LinearLayout
    android:theme="@android:style/Theme.DeviceDefault.DayNight"
    android:background="? android:attr/colorBackground">
    <ImageView
        android:tint="? android:attr/colorAccent" /></LinearLayout>
Copy the code

As you can see, when the theme properties are set, the Widget extracts the main color directly from the system wallpaper and applies it to the dark and light theme backgrounds.

Responsive layout

Android 12 introduces new apis for responsive layouts that automatically switch to different layouts as widgets size. As shown in the figure below, the user can drag to change the size of the Widget, and the Widget dynamically updates the content to be displayed based on the size.

For example, we define three different parameters, including the minimum supported width and height, and the corresponding RemoteView within this size range. The system automatically adjusts the widgets based on the actual size.

val viewMapping: Map<SizeF, RemoteViews> = mapof(
    SizeF(180.0 f.110.0 f) to RemoteViews(
        context. packageName,
        R.layout.widget_small
    ),
    SizeF (270.0 f.110.0 f) to RemoteViews(
        context.packageName,
        R.layout.widget_medium
    ),
    SizeF(270.0 f.280.0 f) to RemoteViews(
        context.packageName,
        R.layout.widget_large
    )
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
Copy the code

Android 12 also provides new targetCellWidth and targetCellHeight attributes, which specify the default larger cell size when a Widget is placed on the home screen. Prior to Android 12, you can use the minWidget and minHeight properties, which specify the default Widget size in dp, and we recommend specifying both properties to maintain backward compatibility. If your Widget is resizable, you can also use the minResizeWidget/Height and maxResizeWidget/Height attributes provided with Android 12 to limit the resizable size range of your Widget.

<appwidget-provider
    android:targetCellWidth="3"
    android: targetCellHeight="2"
    android:minWidth="140dp"
    android:minHeight="110dp"
    android:maxResizeWidth="570dp"
    android:maxResizeHeight="450dp"
    android:minResizeWidth="140dp"
    android:minResizeHeight="110dp"... >
Copy the code

The Widget selector

Android 12 also improves the Widget selector experience by introducing two new properties. The first property is Description, which describes what the Widget selector does. The other is previewLayout, which specifies the XML layout displayed in the Widget selector. Prior to Android 12, you could use the previewImage property to specify static resources to achieve a similar effect, but previewLayout is much more precise and convenient. Also, because these previews are built at run time, they can be dynamically adapted to the theme of the device.

<appwidget-provider android:description= "@string/app_widget_weather_description" android:previewLayout= "@ layout/widget_weather_forecast_small"... />Copy the code

Delta description attribute

Delta previewLayout properties

Many of the new apis introduced in Android 12 have been introduced so far, and we expect to see more and more applications using the new apis to build a more modern Widget experience in the near future.

Glance

In addition to today’s more modern apis, we need more modern and better tools to help us build great widgets, and Glance is a great tool that joins the Jetpack family. Glance is the composable API supported by the Compose Runtime that allows you to create appwidgets using the Compose style syntax, which means you can build interfaces with composable components from Glance, Convert it to a remote view to display in the Widget, and use the new Android 12 API mentioned earlier to make it as backward compatible as possible. Glance also takes care of the Widget lifecycle and other common operations, if that sounds convenient.

△ Glance structure diagram

Next, we’ll show you how to build widgets using Glance. First, you still need to declare the AppWidget as before and link it to the receiver in the AndroidManifest. Of course, Here we use GlanceAppWidgetReceiver and GlanceAppWidget provided by Glance. Glance does most of the work for you. You just overwrite the Content method in MyAppWidget. Provide the content of the AppWidget. Instead of using the XML syntax when defining content, use the Compose syntax, and the content to be displayed will be converted into a remote view for display in the AppWidget.

class MyAppWidget: GlanceAppWidget() {
    @Composable
    override fun Content(a) {
        // Create the AppWidget hereColumn( modifier = Modifier.expandHeight().expandWidth(), verticalAlignment = Alignment.Top, HorizontalAlignment = Alignment. CenterHorizontally) {Text (Text = "Where to" modifier = modifier. The padding12.dp))
            userDestinations()
        }
    }
}
 
class MyAppWidgetReceiver: GlanceAppWidgetReceiver() {
    // Tell MyAppWidgetReceiver which GlanceAppWidget to use
    override val glanceAppWidget: GlanceAppWidget = MyAppWidget()
}
Copy the code

It is important to understand that although Glance uses the Compose Runtime and Compose syntax, it is still a stand-alone framework and you cannot reuse the components defined in the Jetpack Compose UI due to the limitations of building remotely. But if you’re already familiar with Jetpack Compose, Glance will be easy to understand.

In addition, since Glance uses the user event API to handle interactions, it makes it much easier to handle interactions with users. If you understand how widgets work, you know that widgets work on different processes, which makes it difficult to handle even simple user events because not being in the same process means you don’t own the Widget and can only handle events through process callbacks.

Glance abstracts this complexity out by simply defining the Clickable modifier to the composable object that you want to enable it to handle user clicks. Glance abstracts the entire injection behavior from the composanle that the user clicks on, You can call back the defined operation. We also define some common actions, such as how to start an Activity by calling the launchActivity to pass the Activity target class.

Button(
    text = “Home”,
    modifier = Modifier.clickable(launchActivity<NavigationActivity>)
)
Copy the code

In addition, we can provide custom actions to execute some custom code. For example, we might want to update the location and refresh the Widget every time the user clicks the button, as shown in the following code. Glance takes care of some of the work that needs to be injected for you behind the scenes and handles the click through the broadcast receiver. Finally, the action code you defined is invoked. Note, however, that if this is a time-consuming operation such as a network request or database access, use the WorkManager API.

Button(text = "My Location", Modifier = modifier. Clickable (customAction<UpdateLocationAction>)Copy the code

As mentioned earlier, you can use resizable widgets, but dealing with different responsive layouts is no easy task, and Glance tries to make it a little easier by defining three different SizeMode options.

Sizemode. Single is the default option, which specifies that the Widget Content we define here will not change due to available size changes. This means that the minimum supported size we define on Widget metadata will only be called once via the Content method. If the available size of the Widget changes, for example if the user resizes the Widget, the content is not refreshed. As shown in the figure below, a Widget with the Sizemode. Single option will never change its output size no matter how it changes, because the Content method is called only once and the Content is not refreshed when the size changes.

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Single
 
    @Composable
    override fun Content(a) {
        val size = LocalSize.current
        / /...}}Copy the code

△ SizeMode.Single option schematic

If the content is refreshed every time the size changes, use the sizemode.exact option. This option recreates the Widget interface and calls the Content method again each time the user resizes the Widget, providing the maximum available size so that we can change the interface if we have enough space, such as adding additional buttons, and so on. As shown in the figure below, the internal output of the Widget changes as the size changes, because the Widget interface is recreated each time.

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Exact
 
    @Composable
    override fun Content(a) {
        val size = LocalSize.current
        / /...}}Copy the code

△ SizeMode.Exact option schematic diagram

Although the sizemode. Exact option seems to meet the requirements, the interface needs to be re-created every time, which may cause the interface to change when the user adjusts the size because of some performance issues. In this case, we can use the Sizemode. Responsive option. For example, here we map some dimensions to certain shapes. Whenever an AppWidget is created or updated, Glance calls the Content method defined by each Size. Each time, the Content method is mapped to a specific Size and stored in memory. Choosing the most appropriate size based on the available size provides smoother transitions and better performance without having to reinvent the interface. As you can see in the figure below, when a Widget size changes, its internal output changes only if its size matches the predefined size range, and it is important to note that the interface is not recreated.

△ SizeMode.Responsive options schematic diagram

Also, you can define more diversified styles in the Content() method to make widgets display more unique Content at different sizes.

class MyAppWidget: GlanceAppWidget() {
    companion object {
        private val SMALL_SQUARE = DpSize (100.dp, 160. dp)
        private val HORIZONTAL_RECTANGLE = DpSize (250.dp, 100.dp)
        private val BIG_SQUARE = DpSize (250.dp, 250.dp)
    }
 
    override val sizeMode = SizeMode.Responsive(
        SMALL_SQUARE, HORIZONTAL_RECTANGLE, BIG_SQUARE
    )
 
    @Composable
    override fun Content(a) {
        val size = LocalSize.current
        / /...}}Copy the code

In addition to the above, there’s more to explore, such as support for Widget state management, and out-of-the-box Material You theme backgrounds.

To learn more, check out the Android developer website: App Widgets Overview. We look forward to you trying out our new apis and seeing the widgets you build and your feedback.

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!