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 4 of 5 of this article. Click here to view the first page.

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.

Matei Suica

Contributors

Matei Suica

Author

Massimo Carli

Tech Editor

Joe Howard

Final Pass Editor

Over 300 content creators. Join our team.