Android App Widgets Tutorial

Learn how to give your users fast access to the most important functions of your Android app, right from their home screen, using App Widgets. By Matei Suica.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Run your Widget

The Widget you’ve created is beautiful, but it’s not doing anything quite yet. Build and run your app to make sure there’s no error in the XML. Just to be sure everything is fine, add the widget to the screen. If you’ve never added a widget to your Home screen before, here are the steps:

  1. Go to the Home screen
  2. Long press on an empty space
  3. Select “Widgets”
  4. Long press on the Coffee Log Widget
  5. Drop it wherever you like on the screen

Your widget looks like this:

First Widget Run

Notice how the autogenerated code populated the first TextView with “EXAMPLE”. Later in this tutorial, you will update it with the right number of coffee grams.

Performing actions

Now it’s time to add some interactivity to the Widget. When the user selects a button, you’ll have to open MainActivity, passing information about the selected coffee in order to update the total number of grams in today’s record.

Unfortunately, launching a simple Intent is not enough, because we have to remember that our Widget is running in an application that is different from ours and runs in another Android process. The Android platform has a solution for this called PendingIntent that is basically a way to ask another application to launch an Intent for you.

Open then the CoffeeLoggerWidget.kt file and add this utility function at the end of the companion object:

private fun getPendingIntent(context: Context, value: Int): PendingIntent {
  //1
  val intent = Intent(context, MainActivity::class.java)
  //2
  intent.action = Constants.ADD_COFFEE_INTENT
  //3
  intent.putExtra(Constants.GRAMS_EXTRA, value)
  //4
  return PendingIntent.getActivity(context, value, intent, 0)
}

This Kotlin function has the responsibility of creating a PendingIntent for a given coffee:

  1. First you define the Intent to launch as usual using the destination class as argument; in your case it’s the MainActivity class.
  2. The MainActivity can be launched in different ways, and you need something that identifies how much to vary the coffee content. To do this you use an action MainActivity can recognise.
  3. You also need to put into the Intent the quantity to add. Remember, MainActivity doesn’t know what button was pressed on the Widget!
  4. Create the PendingIntent and return it to the caller of the function

Since you now have the action prepared, attach them to the buttons. Go to the updateAppWidget() function in the companion object and add the following code just before its last instruction appWidgetManager.updateAppWidget(...):

views.setOnClickPendingIntent(R.id.ristretto_button, 
    getPendingIntent(context, CoffeeTypes.RISTRETTO.grams))
views.setOnClickPendingIntent(R.id.espresso_button, 
    getPendingIntent(context, CoffeeTypes.ESPRESSO.grams))
views.setOnClickPendingIntent(R.id.long_button, 
    getPendingIntent(context, CoffeeTypes.LONG.grams))

It is worth noting that updateAppWidget() is a convenience method the Android Studio wizard created in order to encapsulate the update logic for a given Widget. Looking at the same Kotlin class, you see that it’s invoked in the onUpdate() method for each Widget that requires an update. This call also happens when the Widget appears in the hosting application for the first time.

override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
  // There may be multiple widgets active, so update all of them
  for (appWidgetId in appWidgetIds) {
    updateAppWidget(context, appWidgetManager, appWidgetId)
  }
}

The RemoteViews class

Now your code should look like this:

internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager,
                                 appWidgetId: Int) {
  //1
  val widgetText = context.getString(R.string.appwidget_text)
  //2
  val views = RemoteViews(context.packageName, R.layout.coffee_logger_widget)
  //3
  views.setTextViewText(R.id.appwidget_text, widgetText)
  //4
  views.setOnClickPendingIntent(R.id.ristretto_button, 
      getPendingIntent(context, CoffeeTypes.RISTRETTO.grams))
  views.setOnClickPendingIntent(R.id.espresso_button, 
      getPendingIntent(context, CoffeeTypes.ESPRESSO.grams))
  views.setOnClickPendingIntent(R.id.long_button, 
      getPendingIntent(context, CoffeeTypes.LONG.grams))
  // 5
  appWidgetManager.updateAppWidget(appWidgetId, views)
}

Here’s what’s going on:

  1. You’re using the Context in order to access a string resource.
  2. An instance of the RemoteViews class is created and given the widget’s layout id. A RemoteViews is basically a mirror image of what you’re going to display in the Widget.
  3. You set the previous string as content of the TextView with id R.id.appwidget_text. It’s very important to note that you can’t access the TextView directly and that only some operations are allowed using the RemoteViews; in this case you’re setting a text.
  4. Using the RemoteViews instance, you register a PendingIntent to use when the user clicks on a each Widget button.
  5. The last instruction binds the specific instance of RemoteViews to the specific instance of the Widget.

Build and run now. You won’t see any difference in the widget, but clicking the Widget buttons will open the app with an updated value of grams. Great job!

Updating the Widget

Widgets should always display the lastest available information, and the update frequency depends on the specific type of data. A Weather Widget doesn’t need a very frequent update, unlike the score of a football match or the price of a specific stock.

You need a way to invoke the previous onUpdate() method at a specific time interval in order to create the new RemoteViews with the new data.

The following drawing gives you an idea of the process:

Diagram for widget updates

The problem is how to send the “I need a refresh!” message to the Widget.

Widget configuration

When the update frequency you need is longer than 30 minutes, you don’t need to write any code and you can simply rely on the configuration file coffee_logger_widget_info.xml Android Studio generated in the res\xml folder.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialKeyguardLayout="@layout/coffee_logger_widget"
  android:initialLayout="@layout/coffee_logger_widget"
  android:minHeight="110dp"
  android:minWidth="180dp"
  android:previewImage="@drawable/example_appwidget_preview"
  android:resizeMode="horizontal|vertical"
  android:updatePeriodMillis="86400000"
  android:widgetCategory="home_screen">
  </appwidget-provider>

The Widget refresh rate is the one defined in the attribute android:updatePeriodMillis. The default value is one day in milliseconds.

Managing updates requests

If you understand how the Android platform manages updates to your Widget, you can replicate the same thing at will. The Android Studio wizard created the CoffeeLoggerWidget class that extends AppWidgetProvider, but we didn’t realize that this was a particular implementation of a BroadcastReceiver.

You can see that by looking at the updates the wizard made to the AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.raywenderlich.android.coffeelogs">

  - - - -

  <receiver android:name=".CoffeeLoggerWidget">
    <intent-filter>
      <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <meta-data
      android:name="android.appwidget.provider"
      android:resource="@xml/coffee_logger_widget_info" />
  </receiver>

  - - - -

</manifest>

Based on the specific Intent‘s action, the AppWidgetProvider dispatches the call to a different methods. Launching an Intent with the android.appwidget.action.APPWIDGET_UPDATE action results in the invocation of the onUpdate() function.

This is exactly what the Android system does at the interval set in the coffee_logger_widget_info.xml configuration file. This means that the updateAppWidget() function is the perfect place for the code to execute on every update.

So add the following line to the beginning of the function:

val coffeeLoggerPersistence = CoffeeLoggerPersistence(context)

and change widgetText to take the value from there:

val widgetText = coffeeLoggerPersistence.loadTitlePref().toString()

Good! Build and run and you’ll see that the widget is periodically updating the “grams” value. Seems like someone had a little too much coffee:

Widget Updates

Matei Suica

Contributors

Matei Suica

Author

Massimo Carli

Tech Editor

Joe Howard

Final Pass Editor

Over 300 content creators. Join our team.