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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android App Widgets Tutorial
35 mins
- Getting started
- App widget anatomy
- User interface
- Resizability and preview
- Refreshing the widget
- Widget customisation
- Create your Widget
- Customizing the User Interface
- Thinking vertically
- Adding buttons
- Run your Widget
- Performing actions
- The RemoteViews class
- Updating the Widget
- Widget configuration
- Managing updates requests
- Update the widget manually
- Communicating via Service
- Making it personal
- Creating a preferences screen
- Know your limits
- Linking preferences to the widget
- Best practices
- Where to go from here
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:
- Get the
AppWidgetManager
instance, which is responsible for all the installed Widgets. - Ask for the identifiers of all the instances of your widget (you could add more than one to your homescreen).
- Create an
Intent
with theandroid.appwidget.action.APPWIDGET_UPDATE
action asking for an update. - Add the
ids
of the widgets you are sending theIntent
to as extras of theIntent
for theAppWidgetManager.EXTRA_APPWIDGET_IDS
key. - 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.
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:
- Check that the array of
allWidgetIds
was in theIntent
. - Loop through the
allWidgetIds
list. - 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:
- It takes a quote array from the strings file
- It picks a random number
- 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
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:
- Find the
EditText
in the layout. - Get the extras from the
Intent
that launched theActivity
. - Extract the
appWidgetId
of the widget. - 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:
- Get the text input – the coffee limit.
- Save the limit to local storage (using the Widget id).
- Create a new
Intent
to return to the caller of theActivity
and add the id of the Widget you’re configuring. - Tell the operating system that the configuration is OK. Do this by passing an
Intent
that contains the widget id. - 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.