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.

Version

  • Kotlin 1.2, Android 4.4, Android Studio 3

The most successful applications are often the simplest to use. This means that users want to see the information they need “at-a-glance” without unlocking their phone or launching the related app. On the Android platform you can achieve this in two different ways. The first, and most recent, is Android Wear, and you can learn more about in Getting Started with Android Wear with Kotlin. The second, the topic of this tutorial, is through the implementation of App Widgets. App Widgets have been available in the Android ecosystem since version Android 1.6 (Donut).

In this tutorial you’ll create an App Widget for a Coffee Log application that will allow you to control your daily usage of caffeine right from your home screen. :]

Note: Most developers love coffee, but we also know that health is very important, so I advise you to read the interesting article Health and Fitness for Developers

You’ll follow the typical process for Widget development and learn how to:

  • Create the Widget user interface
  • Get up-to-date information in the Widget
  • Interact with the Widget

If you’re new to Android Development, I recommended that you read Beginning Android Development with Kotlin before you start, as well as Kotlin for Android.
For this tutorial you’ll also need Android Studio 3.1.2 or later.

Getting started

The first thing you should do is to download the sample project for this tutorial using the download button at the top or bottom of the tutorial. The zip file contains Android Studio projects for the starter and final versions of the Coffee Log application.

Unzip the file in a folder of your choice, go to File/Open or choose “Open an existing Android Studio project” from the Welcome to Android Studio window, and select the build.gradle file in the root folder of the starter project.

File Open

Select build.gradle

Once the project finishes loading and performing a Gradle build, you can have a look at the file structure, which should be like this:

File structure

Now that you are in the project, take a look around, especially in MainActivity, where all the logging happens. CoffeeTypes is a simple enum class with all the coffee types and their caffeine quantity in grams, while the CoffeeLoggerPersistence class is managing persistence using SharedPreferences.

It’s time to start tracking our caffeine consumption! Build and run the app by going to the Build\Make Project or using the green “play” button from the toolbar. The app will appear in your emulator or device, looking like this:

The app allows you to see how many grams of coffee you drank so far today and select new drinks to update your consumption count. Each selection leads to an update of the total displayed.

To use the app to log your coffee consumption, you have to launch the full application. As always, we can do better. What about making your user’s life simpler with an App Widget like this one?

Widget idea

With a Widget, you can access the same information as the application, and display a powerful motivational quote, just by using your device home screen. As you can see the layout is different because the list is now a set of 3 buttons.

There’s a lot to cover to create an App Widegt, so let’s dig in!

App widget anatomy

As the Android documentation says, an App Widget is a component that can be embedded in other applications, typically the Home screen. Security and performance are very important, so the Android platform has defined a very clear protocol that describes how an App Widget communicates with its own app and interacts with the hosting one. This is why the developer has to provide a configuration file with the following information:

  • The Widget layout
  • The Widget screen space
  • Whether the Widget can resize and how
  • A preview image that users will see when dragging the Widget on the screen
  • How often refreshing data can happen
  • An optional Configuration screen

As you’ll see, the Android system uses this information in different stages of the Widget lifecycle. The layout information is useful when the Widget is running and interacting with the user. Resize, preview and screen space required are useful when the user decides to select the Widget and drag it into the Home screen.

User interface

As you’ve seen in the previous images, apps and Widgets have different UIs. This is because the available space is different, as well as the user interaction modes. For both apps and Widgets, you can define the layout using a resource file.

You have to remember that a Widget is running in a different application and so some restrictions are in place for security and performance reasons. This means that you can only use a subset of the standard components, with which you can then interact only using a specific object of type RemoteViews. In particular, you can use only:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

Along with ViewStub, which allows a lazy inflation of a layout, you can only use the following containers:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

Extensions of these classes are not allowed.

The check on these constraints is strong. Because of these restrinctions, a Widget layout has to be very simple and only use simple components like TextView, Button or ImageView.

Resizability and preview

The configuration file is the mechanism used to describe your Widget to the Android system. You can use this for setting the supported Widget sizes, telling the system whether the Widget is resizable or not, and providing an image to display when the user decides to add a Widget to their Home screen. You’ll see all of these when you insert your Widget for the first time.

Refreshing the widget

The data the Widget displays must always be up to date without wasting system resources. This means that the UI should be updated only when the data changes, and this can happen for different reasons. If the user interacts with the Widget, you need a way to update the UI and then send the event to the main app. If something is happening in the main app, you need a way to tell the Widget to refresh.

The Android platform also provides a third way, an automatic refresh of the Widget at an interval that can be set using the configuration file. Performance limitations don’t allow an update frequency greater than 30 minutes.

Widget customisation

In the case of Coffee Log, there are just three different type of coffees. But what if the user is not interested in Long coffee or they just want a different drink instead, or what if they want to simply change the quantity of grams. Or maybe the user wants to customise the background color of the Widget. As you’ll see, it’s possible to provide a configuration screen to allow all the needed customisation.

Create your Widget

Enough theory, now you can start creating your Widget. Creating a Widget requires the definition of some code and configuration files according to the specification defined by the Android platform.

Android Studio makes this process very easy, through the usage of a simple wizard, which you can access by selecting New\Widget\App widget from the File menu. You’ll see the following window:

New Android component

Add the following input to the window:

  • Class name: CoffeeLoggerWidget
  • Minimum Width (cells): 3
  • Minimum Height (cells): 2

Here you can also see how it’s possible to define whether the Widget is resizable and what its possible destinations are. A Widget is usually part of the Home screen, but it could also part of the Keyguard, which is the screen that appears when the phone is locked.

Select Finish, and Android Studio will create three files for you:

  • CoffeeLoggerWidget.kt: this is a Kotlin class with the same name used in the wizard, and acts as the controller for the Widget. You’ll learn how to change this code in order to access the UI component through the RemoteViews class and how to receive and manage events from the Widget itself.
  • coffee_logger_widget_info.xml: this is the configuration file we described earlier with information about the refresh rate, resizability, dimensions, etc. This is the file you’re going to edit in order to provide a configuration Activity for the Widget.
  • coffee_logger_widget.xml: this file contains the widget’s user interface layout.

It’s important to note where all these files are in the project structure:

New files

In particular, you see how the configuration file has been created as an XML resource file.

As you’ll see later, the wizard also made some changes to the app AndroidManifest.xml file.

Customizing the User Interface

In order to customize the UI for the Widget, open coffee_logger_widget.xml in the app\res\layout folder. The Android Studio wizard generated the following layout that you need to update:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="#09C"
  android:padding="@dimen/widget_margin">

  <TextView
    android:id="@+id/appwidget_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:layout_margin="8dp"
    android:background="#09C"
    android:contentDescription="@string/appwidget_text"
    android:text="@string/appwidget_text"
    android:textColor="#ffffff"
    android:textSize="24sp"
    android:textStyle="bold|italic" />

</RelativeLayout>

Remove the TextView and replace the RelativeLayout with a LinearLayout. In Android Studio, you can do this by double-clicking on the old name and typing the new name in its place. After this change you should have this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="#09C"
  android:padding="@dimen/widget_margin">
</LinearLayout>

Note: You’re going to use styles that are already defined in the sample project. They contain text sizes and colors, heights, widths, alignments, and other style values. If you are curious about them, check out styles.xml in the res/values folder.

Next, add three more attributes to the LinearLayout:

  ...
  android:id="@+id/widget_layout"
  android:orientation="vertical"
  android:gravity="center"
  ...

The android:orientation and android:gravity attributes give the LinearLayout information about how to align its content. Providing an id is also important in case we need to get a reference to the layout in the Kotlin code.

To achieve rounded corners, change the android:background attribute to @drawable/background, a drawable available in the starter project. Now the root element of the layout looks like this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/widget_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@drawable/background"
  android:gravity="center"
  android:orientation="vertical"
  android:padding="@dimen/widget_margin">
</LinearLayout>

Thinking vertically

For the sake of aesthetics, the user interface should look good regardless of the Widget size. It’s best to have the Widget elements spread over the available space. There are many ways to achieve that, but you should go for the simplest which consists of adding some TextView components that will expand in the remaining space between the rest of the elements.

Here’s a schematic of the layout you’ll create:

Widget Schematic

The green pattern will be a TextView that expands vertically and the blue pattern will be a TextView that expands horizontally. Keep this schematic in mind as you build the layout to understand why you add each element.

Note:If you’re tempted to fill the empty spaces using a Space instead of TextView, remember that a Widget has some UI restrictions and that a Space is not one of the allowed components.

The first element in the LinearLayout is a vertical space that you can define by adding this code as the first child:

<TextView style="@style/WidgetButtonVerticalSpace" />

Now you can add the TextView components for the amout of coffee:

  <TextView
    android:id="@+id/appwidget_text"
    style="@style/WidgetTextView.Big" />

  <TextView
    style="@style/WidgetTextView"
    android:text="@string/grams" />

Then add another TextView for the next vertical space before the buttons:

<TextView style="@style/WidgetButtonVerticalSpace" />

Notice that the first text view needs to have an id because you will need to change the text later on from the Kotlin code. The second one is fixed text. You’re using the predefined styles on the text views.

Next, add a container for the buttons as a LinearLayout with horizontal orientation:

<LinearLayout
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal">

    <!-- Buttons go here -->

</LinearLayout>

Then a TextView for the quote after the last vertical space.

<TextView style="@style/WidgetButtonVerticalSpace" />

<TextView
   android:id="@+id/coffee_quote"
   style="@style/WidgetQuote" />

Adding buttons

Now the green part of the layout is fnished and you have to deal with the blue part for the buttons following this schematic:

Widget buttons

You’ve already created a container for them so you just need to start with a TextView that expands horizontally and will keep the first button at a distance from the left margin:

<TextView style="@style/WidgetButtonHorizontalSpace" />

Then you can add the first button for smallest coffee in the world:

<LinearLayout
  android:id="@+id/ristretto_button"
  style="@style/WidgetBeverageButton" >

  <ImageView
    style="@style/WidgetButtonImage"
    android:src="@drawable/ic_ristretto" />
  <TextView
    style="@style/WidgetButtonText"
    android:text="@string/ristretto_short" />

</LinearLayout>

<TextView style="@style/WidgetButtonHorizontalSpace" />    

Each button has a LinearLayout that contains an ImageView and a TextView. After the button, you added another horizontally expanding TextView to help the buttons spread.

Add the next button for Espresso:

<LinearLayout
  android:id="@+id/espresso_button"
  style="@style/WidgetBeverageButton">

  <ImageView
    style="@style/WidgetButtonImage"
    android:src="@drawable/ic_espresso" />
  <TextView
    style="@style/WidgetButtonText"
    android:text="@string/espresso_short" />

</LinearLayout>

<TextView style="@style/WidgetButtonHorizontalSpace" />

And the final button for the Long:

<LinearLayout
  android:id="@+id/long_button"
  style="@style/WidgetBeverageButton" >

  <ImageView
    style="@style/WidgetButtonImage"
    android:src="@drawable/ic_long_coffee" />
  <TextView
    style="@style/WidgetButtonText"
    android:text="@string/long_coffee_short" />

</LinearLayout>

<TextView style="@style/WidgetButtonHorizontalSpace" />

Phew! That was long but you’re done with the layout for the widget. :]

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

Update the widget manually

If your app needs to update the data in the Widget more frequently, you already have the solution: you can simply periodically launch the same Intent the Android system does. In the case of the Coffee Log application this happens every time the user selects a coffee in the app.

Open MainActivity and add the following code at the end of refreshTodayLabel:

// Send a broadcast so that the Operating system updates the widget
// 1
val man = AppWidgetManager.getInstance(this)
// 2
val ids = man.getAppWidgetIds(
    ComponentName(this, CoffeeLoggerWidget::class.java))
// 3
val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
// 4
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
// 5
sendBroadcast(updateIntent)

Since this code has some new elements, let me walk you through it:

  1. Get the AppWidgetManager instance, which is responsible for all the installed Widgets.
  2. Ask for the identifiers of all the instances of your widget (you could add more than one to your homescreen).
  3. Create an Intent with the android.appwidget.action.APPWIDGET_UPDATE action asking for an update.
  4. Add the ids of the widgets you are sending the Intent to as extras of the Intent for the AppWidgetManager.EXTRA_APPWIDGET_IDS key.
  5. Finally, send the broadcast message.

Build and run tha app to check that everytime you add some coffee, the widget also updates.

Communicating via Service

Not all the updates needed for Widgets are a consequence of an action from the user. Typical cases are data from a server through periodic polling and push notification events. In cases like these, the request has to come from a different component, which you usually implement as an Android Service.

Choose File\New\Service\Service and change the name to CoffeeQuotesService.

New Service

When you click Finish, Android studio generates a Kotlin file for you for the Service.

In CoffeeQuotesService, replace the current implementation of onBind() with:

return null

Change the return type of onBind to be the nullable IBinder?.

Then add this function, which is the one the Android system invokes at every launch of the service Service:

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  val appWidgetManager = AppWidgetManager.getInstance(this)
  val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
  //1
  if (allWidgetIds != null) {
    //2
    for (appWidgetId in allWidgetIds) {
      //3
      CoffeeLoggerWidget.updateAppWidget(this, appWidgetManager, appWidgetId)
    }
  }
  return super.onStartCommand(intent, flags, startId)
}

You’ve seen the first two lines before. The others do the following:

  1. Check that the array of allWidgetIds was in the Intent.
  2. Loop through the allWidgetIds list.
  3. Update each widget.

Now, you need to call this service instead of directly updating the widget. Open CoffeeLoggerWidget and replace the content of onUpdate() with the following in order to start the Service:

val intent = Intent(context.applicationContext, CoffeeQuotesService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
context.startService(intent)

This creates an Intent, puts the Widget ids in the intent, and starts the Service.

In the companion object, add the following function:

private fun getRandomQuote(context: Context): String {
  //1
  val quotes = context.resources.getStringArray(R.array.coffee_texts)
  //2
  val rand = Math.random() * quotes.size
  //3
  return quotes[rand.toInt()].toString()
}

This function generates a random coffee quote:

  1. It takes a quote array from the strings file
  2. It picks a random number
  3. Finally, it returns the string at the random position

After you have the string, update the widget. In updateAppWidget() add this before the last call:

views.setTextViewText(R.id.coffee_quote, getRandomQuote(context))

That’s it. Every time the widget updates, you get a new quote!

Making it personal

People like to personalize the look and functionality of their Home screens, and Widgets are no exception. You have to take into account that a general purpose Widget won’t bring much value to a user. To make it personal you need to let the users set up preferences and configurations.

Earlier, when covering the configuration of a Widget, you learned that it can have a Configuration screen. This is an Activity that is automatically launched when the user adds a Widget on the home screen. Note that the preferences are set up per Widget because users can add more than one instance of a Widget. It’s better to think about saving this preferences with the id of the Widget.

In this project, the configuration screen could contain a coffee amount limit. If the user logs more coffee than the limit, the Widget will turn into a soft but alarming pink.

Creating a preferences screen

The preference screen for a Widget is an Activity. Choose New\Activity\Empty activity from the File menu and edit the fields to be

  • Activity name: CoffeeLoggerWidgetConfigureActivity
  • Layout Name: activity_coffee_logger_widget_configure

Configure Activity

Make sure the Launcher Activity checkbox is unchecked and the Source Language is Kotlin.

When you click Finish, Android Studio will generate the code for the new Activity and a template for the layout file, along with adding the registration of the Activity in the AndroidManifest.xml file.

Now create the layout for the configuration screen. Open activity_coffee_logger_widget_configure.xml and add the following:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical"
  android:padding="16dp">

  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:labelFor="@+id/appwidget_text"
    android:text="@string/coffee_amount_limit" />

  <EditText
    android:id="@id/appwidget_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="number" />

  <Button
    android:id="@+id/add_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="@string/save_configuration" />
</LinearLayout>

The layout is nothing complicated: a TextView that represents a label to the EditText, and a Button for the user to save the preferences.

Know your limits

Open CoffeeLoggerWidgetConfigureActivity and add these fields above onCreate() (developers usually put fields at the beginning of the class):

private lateinit var appWidgetText: EditText
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private val coffeeLoggerPersistence = CoffeeLoggerPersistence(this)

You will need to use these fields later to save the limit value for each widget.

In onCreate(), add the following code at the end:

//1
appWidgetText = findViewById(R.id.appwidget_text)
//2
val extras = intent.extras
//3
appWidgetId = extras.getInt(
    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
//4
setResult(Activity.RESULT_CANCELED)

Here’s what the code does:

  1. Find the EditText in the layout.
  2. Get the extras from the Intent that launched the Activity.
  3. Extract the appWidgetId of the widget.
  4. Make sure that if the user doesn’t press the “Save Configuration” button, the widget is not added.

Finally, you need to save the configuration when the user presses the “Save Configuration” button. Below onCreate(), declare the following OnClickListener implementation:

private var onClickListener: View.OnClickListener = View.OnClickListener {
  // 1
  val widgetText = appWidgetText.text.toString()
  // 2
  coffeeLoggerPersistence.saveLimitPref(widgetText.toInt(), appWidgetId)
  // 3
  val resultValue = Intent()
  resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
  // 4
  setResult(RESULT_OK, resultValue)
  // 5
  finish()
}

Here you:

  1. Get the text input – the coffee limit.
  2. Save the limit to local storage (using the Widget id).
  3. Create a new Intent to return to the caller of the Activity and add the id of the Widget you’re configuring.
  4. Tell the operating system that the configuration is OK. Do this by passing an Intent that contains the widget id.
  5. Close the configuration screen

Attach this listener to the button by adding the following line below setContentView() in onCreate():

findViewById<View>(R.id.add_button).setOnClickListener(onClickListener)

This is a chained instruction that finds the Button object and sets its listener.

Linking preferences to the widget

It is a good idea to refresh the widget after the user saves the preferences. That’s because the limit might already be exceeded at the moment of adding a new widget. For this reason, write another method at the end of CoffeeLoggerWidgetConfigureActivity to trigger the refresh:

private fun updateWidget() {
  val appWidgetManager = AppWidgetManager.getInstance(this)
  CoffeeLoggerWidget.updateAppWidget(this, appWidgetManager, appWidgetId)
}

The function retrieves the AppWidgetManager and triggers an update to the corresponding widget. Call this function in the OnClickListener after saving the coffee limit to coffeeLoggerPersistence. It should be before creating the Intent:

updateWidget()

To launch the configuration screen whenever the user adds a widget, you need to add it to the widget configuration file. With this in mind, open coffee_logger_widget_info.xml and add the following attribute to appwidget-provider:

android:configure="com.raywenderlich.android.coffeelogs.CoffeeLoggerWidgetConfigureActivity"

Build and run, then go to the home screen. Long press the widget and drag it to the “Remove” area. Add another widget as before and check that the configuration screen appears. It should look like this:

Configuration Screen

Enter a value in the field like 10 and press “Save configuration” to add the widget.

To make the widget react to the limit, add this in CoffeeLoggerWidget inside updateAppWidget*(, before the last line:

// 1
val limit = coffeeLoggerPersistence.getLimitPref(appWidgetId)
// 2
val background = if (limit <= widgetText.toInt()) R.drawable.background_overlimit 
    else R.drawable.background
// 3
views.setInt(R.id.widget_layout, "setBackgroundResource", background)

Step by step:

  1. First, get the limit saved by the user for that widget.
  2. Decide if the user exceeds the limit of coffee and establish one of the two possible backgrounds: pink or blue.
  3. Set the background to the widget's root element.

Finally, build and run. After the app opens log more coffees than the limit you set. Let's say your limit was 10: log three Espresso and go back to the home screen. As a result, your widget is now pink:

Over the limit

Best practices

Some final advice before you start adventuring into the world of Widgets:

  • Design the smallest Widget size you can. Don't take up screen real-estate if you don't need it. Be aware that the user might resize it into a bigger area.
  • Don't refresh the Widget too often because it will drain the battery. On the other hand, don't refresh it too rarely because it won't be useful on the screen.
  • Make sure you read the official guidelines for Widget design and follow the recommendations. Revisit them from time to time because things change and things get added.
  • Think of Widgets as a shortcut window into your app. Provide the most important information and actions in it.

Where to go from here

Congratulations, you've finished your App Widget! Download the final project using the button at the top or bottom of the tutorial.

You learned how to develop an App widget to track your coffee intake. In summary, some of your new skills are:

  • Create a widget layout
  • Link a configuration screen
  • Communicate via a Service

... and tie them all together. This is impressive!

You can learn more about App Widgets by checking out the official docs.

For a better understanding of Intents, have a look at the Android Intents Tutorial.

You can create a better user interface for your apps and widgets with more Material Design. Get a little knowledge boost from Android: An Introduction to Material Design.

If you have any questions or comments about Android App Widgets, please join the forum discussion below!

Contributors

Comments