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.
Once the project finishes loading and performing a Gradle build, you can have a look at the file structure, which should be like this:
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?
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:
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:
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:
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:
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:
- Go to the Home screen
- Long press on an empty space
- Select “Widgets”
- Long press on the Coffee Log Widget
- Drop it wherever you like on the screen
Your widget looks like this:
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:
- First you define the
Intent
to launch as usual using the destination class as argument; in your case it’s theMainActivity
class. - 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 actionMainActivity
can recognise. - You also need to put into the
Intent
the quantity to add. Remember,MainActivity
doesn’t know what button was pressed on the Widget! - 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:
- You’re using the
Context
in order to access a string resource. - An instance of the
RemoteViews
class is created and given the widget’s layout id. ARemoteViews
is basically a mirror image of what you’re going to display in the Widget. - You set the previous string as content of the
TextView
with idR.id.appwidget_text
. It’s very important to note that you can’t access theTextView
directly and that only some operations are allowed using theRemoteViews
; in this case you’re setting a text. - Using the
RemoteViews
instance, you register aPendingIntent
to use when the user clicks on a each Widget button. - 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:
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:
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.
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:
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:
- First, get the limit saved by the user for that widget.
- Decide if the user exceeds the limit of coffee and establish one of the two possible backgrounds: pink or blue.
- 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:
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!
The post Android App Widgets Tutorial appeared first on Ray Wenderlich.