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:
- We can start a service, where the
Intent
identifies the service to be started - We can send a broadcast, where the
Intent
is the broadcast, identifying who should receive the message
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
:
- We call
setAction()
, to associate a custom action string with ourIntent
. We can use this to distinguish cases where theColorWidget
is invoked based on our ownIntent
from cases where it is invoked for other reasons. - We call
putExtra()
to attach the app widget ID to theIntent
. This way, we know which app widget is the one that the user tapped the refresh button on.
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:
- a
Context
- a unique ID for this
PendingIntent
(in this case, we use the app widget ID) - the
Intent
to be wrapped in thePendingIntent
- a flag, the details of which are well beyond the scope of this book
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:
- Confirming that the action string is our custom refresh action, and
- Confirming that we have our app widget ID extra
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:
- The ID of the navigation graph resource that we are using
- The ID of the destination that we are trying to navigate to
- A
Bundle
of arguments to pass to that destination (in this case, packaging up our color)
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.