Actions and App Widgets

App widgets can do more than render a bit of UI on the home screen. They can also respond to click events, either to update the contents of the app widget or to take some other action, such as displaying an activity.

However, the app widget UI is not part of the app’s process. We do not have access to the actual UI objects, so we cannot call setOnClickListener() on them. Instead, we have go through a bit of indirection, in the form of PendingIntent objects.

Introducing PendingIntent

We have seen Intent objects before. We use them to identify activities to start. As it turns out, we can also use Intent objects with two other types of components:

A PendingIntent is a wrapper around an Intent that, among other things, indicates what action should be used with that Intent, such as starting an activity or sending a broadcast. There is a lot more to a PendingIntent than that, particularly with respect to security. But, for the purposes of this chapter, considering it to be a combination of an Intent and a statement of what to do with the Intent will suffice.

A PendingIntent is a way of passing an action to be performed to some other app or to the OS. In the case of app widgets, we can register click “listeners” that, instead of invoking some listener object or lambda expression in our app, invokes a PendingIntent. So, we can tie clicks in our app widget to PendingIntent objects, which in turn tell Android to start an activity, send a broadcast, etc.

Refreshing the Color

With that in mind, let’s look at two actions that we want to perform from our app widget.

First, there is the refresh button on the one side of the app widget. When the user taps that button, we want to generate a new random color and update the UI of the app widget to show that color.

Since we want to update the app widget, one approach is to route control back to ColorWidget, since we already have the logic there to update the app widget UI.

Creating the PendingIntent

In updateWidget(), as part of configuring the RemoteViews, we create an Intent, identifying our ColorWidget:

    Intent refreshIntent = new Intent(context, ColorWidget.class)
      .setAction(ACTION_REFRESH)
      .putExtra(EXTRA_APP_WIDGET_ID, appWidgetId);
    val refreshIntent = Intent(context, ColorWidget::class.java)
      .setAction(ACTION_REFRESH)
      .putExtra(EXTRA_APP_WIDGET_ID, appWidgetId)

We created Intent objects for our own components before, when starting activities. We use the same Intent constructor here as then, to supply our ColorWidget class and indicate that is what should be started. We also do two other things as part of configuring the Intent:

Then, we wrap that Intent in a PendingIntent:

    PendingIntent refreshPI = PendingIntent.getBroadcast(
      context,
      appWidgetId,
      refreshIntent,
      PendingIntent.FLAG_UPDATE_CURRENT
    );
    val refreshPI = PendingIntent.getBroadcast(
      context,
      appWidgetId,
      refreshIntent,
      PendingIntent.FLAG_UPDATE_CURRENT
    )

The function on PendingIntent that we call is getBroadcast(), saying that we want the system to send a broadcast using our supplied Intent. We also provide:

Connecting the PendingIntent

Given the PendingIntent, attaching it to a click listener is a matter of calling setOnClickPendingIntent() on the RemoteViews:

    remoteViews.setOnClickPendingIntent(R.id.refresh, refreshPI);
    remoteViews.setOnClickPendingIntent(R.id.refresh, refreshPI)

setOnClickPendingIntent() takes the ID of the widget in the layout and the PendingIntent. If the user clicks that widget, Android will invoke the PendingIntent and will send our broadcast.

Reacting to the PendingIntent

Our broadcast is going to our ColorWidget as a broadcast. The entry point for a BroadcastReceiver is an onReceive() function. So, ColorWidget has its own onReceive() function to process our broadcast:

  @Override
  public void onReceive(Context context, Intent intent) {
    if (intent.getAction().equals(ACTION_REFRESH) &&
      intent.hasExtra(EXTRA_APP_WIDGET_ID)) {
      AppWidgetManager appWidgetManager =
        context.getSystemService(AppWidgetManager.class);

      updateWidget(
        context,
        appWidgetManager,
        intent.getIntExtra(EXTRA_APP_WIDGET_ID, -1)
      );
    }
    else {
      super.onReceive(context, intent);
    }
  }
  override fun onReceive(context: Context, intent: Intent) {
    if (intent.action == ACTION_REFRESH && intent.hasExtra(EXTRA_APP_WIDGET_ID)) {
      val appWidgetManager =
        context.getSystemService(AppWidgetManager::class.java)

      updateWidget(
        context,
        appWidgetManager,
        intent.getIntExtra(EXTRA_APP_WIDGET_ID, -1)
      )
    } else {
      super.onReceive(context, intent)
    }
  }

onReceive() gets a copy of the Intent that was broadcast. The first thing that we do is to see if this is our refresh broadcast, by:

If we do, we call updateWidget() from here, to update that app widget based on its ID. If we do not, we chain to the superclass implementation of onReceive(). In this case, the superclass is AppWidgetProvider, and it handles other broadcasts… such as the broadcast that eventually triggers the call to onUpdate().

Displaying the BigSwatchFragment

The other action that we want to perform will be triggered by a click anywhere else on the app widget besides our refresh button. That should bring up our screen to show our color, the same as if the user tapped on a row in our list of colors.

In this case, we want to navigate to BigSwatchFragment. We are using Jetpack’s Navigation framework for handling the navigation between fragments. One side benefit of that is we can use NavDeepLinkBuilder to create a PendingIntent for our use:

    Bundle args = new Bundle();

    args.putInt("color", color);

    PendingIntent deepLinkPI = new NavDeepLinkBuilder(context)
      .setGraph(R.navigation.nav_graph)
      .setDestination(R.id.bigSwatchFragment)
      .setArguments(args)
      .createPendingIntent();

    remoteViews.setOnClickPendingIntent(R.id.root, deepLinkPI);
    val deepLinkPI = NavDeepLinkBuilder(context)
      .setGraph(R.navigation.nav_graph)
      .setDestination(R.id.bigSwatchFragment)
      .setArguments(bundleOf("color" to color))
      .createPendingIntent()

    remoteViews.setOnClickPendingIntent(R.id.root, deepLinkPI)

NavDeepLinkBuilder takes:

Then, a simple call to createPendingIntent() will create a PendingIntent that will start our activity and go to our desired destination. We can attach that PendingIntent to the RemoteViews via setOnClickPendingIntent(), and the rest is taken care of for us by the Navigation framework.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.