All About Configurable Widgets
The truth about configurable widgets
Intro
A friend of mine told me about this app he'd like to have. It is a very specific need which probably very few people have → he needed a button that would allow him to start an email with pre-filled data (recipients, subject, etc), so that he could skip a bunch of taps and steps and be as efficient as possible to send himself memos by email.
I like efficiency, I am myself a very big user of such niche and specific apps, and thus I saw this as an opportunity to learn about configurable widgets, and I figured it would be a "nice app" to make as a base for an explanation as to how I went about configurable widgets on Android because frankly, in 7 years developing for the platform, I had never done any widgets before ... shame on me. Since, I actually have made another app with configurable widgets as well ✌ :).
So let's get started.
Foreword(s)
- The app is built on purpose with minimal dependencies:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:recyclerview-v7:23.1.1'
}
- It is ugly (meaning not well designed), I know, it's on purpose (that's my excuse for lacking any kind of UI design genes)
- It's using the best practices that I know, feel free to correct me ... that is how I grow!
- It may be buggy, but it's pretty straight forward so it should be ok.
- It is assumed that you have basic knowledge and Android development to understand this post. Meaning you know how to start a project, what an activity is, and so on.
Where to start?
Step 1: widget's UI
First thing first, you have to understand that a widget UI behave just like any other type of Android UI with I'd say one exception: the size.
Indeed, a widget can have a fixed size, and potentially be expanded. If expansion there is, then you must adapt (or not) the UI accordingly. I will not cover this at the moment (sorry).
So first,you need the widget UI, which is a layout like any other layout file in an app, and it'll look something like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
android:background="@drawable/bg_widget"
>
<ImageView
android:id="@+id/imageIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="5dp"
android:src="@drawable/ic_pen"
android:layout_centerHorizontal="true"
android:contentDescription="@string/app_name"
/>
<TextView
android:id="@+id/textLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/imageIcon"
android:textColor="@color/widgetText"
android:layout_centerHorizontal="true"
tools:text="Label"
/>
</RelativeLayout>
So very simple, one icon on top, one label underneath, a bit of padding and an awesome background that allows for rounded corners ... super classy!
Step 2: the provider configuration
The provider will be the key to interact with the widget: it will manage its creation, update and deletion. It will be up to you to figure out a way to make it do that right.
First you must define the provider creating the AppWidgetProviderInfo
with an XML file located in the res/xml
folder that will be called widget_info.xml
(for lack of more originality), and it'll look like this:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="0"
android:resizeMode="none"
android:previewImage="@drawable/ic_widget"
android:initialLayout="@layout/widget_layout"
android:configure="com.techyos.oneclickonemail.ui.activities.WidgetConfigActivity"
android:widgetCategory="home_screen">
</appwidget-provider>
Explanation of the parameters (because I'm a nice guy, and you may deserve it!)
As per the documentation, the minWidth
and minHeight
in this case will represent a widget that is 1x1 columns on the screen, and it's not resizable (yes I know, I'm lazy), which is why resizeMode
is set to "none".
Next, the updatePeriodMillis
. It is not important in this case as it will manually be triggered for every change by the user, and the data cannot change on its own, so it is set to 0 to avoid useless system overhead and CPU churning.
The previewImage
is the image that will be displayed in the widget picker area.
The initialLayout
is the one we made earlier.
The configure
points to the configuration activity, which is an activity like any other activity, and that will allow you to get whatever configuration you need from the user for that widget. You'll see later that you can re-use this activity in the regular app.
And finally widgetCategory
(which is only available since api level 17 by the way, so take appropriate precautions) defines this widget to be on the home screen only and not the lock screen (which makes no more sense since api level 21 by the way).
Ok now that we have defined our awesome widget ... well we need to put together the stuff that will make it actually work. Which means --> CODE! (woohoo!)
Step 3: provider implementation
It's time for a little of real fun, enough with XML, let's do some Java (sorry Kotlin will be for a later post).
So in order to understand what the provider is, and how it works, you have to understand how a widget works.
A widget is an entity that works independently from an app. You cannot programmatically create or delete it from an app: that is up to the system, and to the end user, to decide to add a widget on the home screen, or remove it for that matter.
And if you want to interact with it from an app or a service, you need some kind of a communication channel, and this channel is the AppWidgetProvider
.
In the previous step, we have created an XML file describing the behavior of the widget, the system will figure things out based on that. Now we have to let the system know what instance of the provider to use in order for the widget to live.
First, we need to ad a few definitions in the AndroidManifest.xml
, starting with the provider declaration which actually is a BroadcastReceiver
:
<receiver android:name=".ui.widgets.WidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
Notice the intent filters, which basically tells the system to wake this instance up when a widget is updated (= also created) or deleted. Then, there is that line (meta-data) that says "and by the way, the configuration is in the xml file mentioned here".
Next thing, while we're in the manifest anyway, let's declare the configuration activity as well:
<activity android:name=".ui.activities.WidgetConfigActivity">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
Again, note the intent filter specifying that the activity will react to a widget configuration intent.
Enough with XML (finally)
So far, everything we have done have been "by the book". All of that can be found pretty much exactly as is in the official documentation. What is not defined in the said documentation is what comes afterwards.
The point of this article is to show how to make a configurable widget, which means you have an activity that allows the user to set some values that will provide a specific behavior for the widget.
We have declared this activity previously in the AndroidManifest.xml
, as well as in the appwidget-provider
XML, and the implementation of that activity is not very important as, I hope, you have already done tons of them. In this example, it is a simple form like activity that will write the data in the database using the received appwidget id as the key.
Now what is tricky is not the activity, it is the implementation of the provider, which we also have declared in the manifest earlier. And here is its implementation.
There are (at least) 2 methods to override in order for everything to work smoothly:
First, the onUpdate
method:
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
WidgetEntryDao dao = new WidgetEntryDao(new DbHelper(context));
final int N = appWidgetIds.length;
// Perform this loop procedure for each App Widget that belongs to this provider
for (int i = 0; i < N; i++) {
int appWidgetId = appWidgetIds[i];
// Finding the widget entry for the app widget id
WidgetEntry entry = dao.findByWidgetId(appWidgetId);
if (entry != null) {
// creating the intent with all the information we want to pre-fill
Intent intent = new Intent(Intent.ACTION_SEND);
// array of destination email addresses
intent.putExtra(Intent.EXTRA_EMAIL, entry.getTo());
// subject (optional)
intent.putExtra(Intent.EXTRA_SUBJECT, entry.getSubjectPrefix());
// little bonus, we're adding some kind of signature after the (optional) message
intent.putExtra(Intent.EXTRA_TEXT, entry.getMessagePrefix() + "\n\n\n" + context.getString(R.string.signature));
// and setting a type that will narrow the choice of apps that handle email related stuff
intent.setType("message/rfc822");
// creating pending intent, the flag is REALLY important, and so is the appWidgetId
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
// using main layout to handle the click, so the whole widget is a button
views.setOnClickPendingIntent(R.id.layoutWidget, pendingIntent);
// setting the label
views.setTextViewText(R.id.textLabel, entry.getLabel());
// updating the widget because
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}
Few details
- I won't get into details with the DAO, it is just my way of dealing with data on Android, I like the separation of concerns. Check out this file for more information.
- This method is called when a
APPWIDGET_UPDATE
intent is broadcasted. In our case it's when the widget is created, and when modification is made from the main app. Remember that we have set aupdatePeriodMillis
to 0, but had you set this value to some other number greater than 0, the intent would have been triggered by the system every that much milliseconds automatically. - About the way to open a pre-filled email app on Android, there are several ways of doing it, I know of 2. The solution displayed may not be the best as it may show some apps that do not send emails in the app selection dialog. But it works, and I can set multiple recipients :).
- The biggest problem I faced was that if I had several widgets with a different configuration, they would all open with the configuration of the first widget. I finally found out about the
PendingIntent.FLAG_UPDATE_CURRENT
flag that is set during the construction of thePendingIntent
instance, which saved my life. Also, theappWidgetId
in there is not mandatory but will help to focus this particularPendingIntent
instance with that particular widget and avoid weird behaviors. This is my version of a winning combination!
Second, the onDelete
method:
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
WidgetEntryDao dao = new WidgetEntryDao(new DbHelper(context));
final int N = appWidgetIds.length;
for (int i = 0; i < N; i++) {
dao.deleteByWidgetId(appWidgetIds[i]);
}
}
Not much to explain here, other than the fact that the method is called when a APPWIDGET_DELETED
intent is broadcasted by the system, meaning when the user is removing the widget from his/her home screen.
Basically here, I'm back again with my DAO object and I delete the widget corresponding to the received id from the database, that's it.
Note: if you try to broadcast this yourself in the app, the system will throw a very nice java.lang.SecurityException: Permission Denial
exception because as stated in the documentation, this intent can only be broadcasted by the system.
Step4: What's left?
So now we have the widget's layout, the configuration, the provider ... what's left?
In our example, the widget is a button, and the button opens up an (email) app installed on the device. But a widget could do all sorts of stuff like displaying information, or sending data to a server, etc.
In this example, I decided that the main app could let the user change the configuration at will, and thus I have a list of all the widget configurations that have been written in the database, and with a tap on one of the list item, the configuration activity is launched again and fills the form entries the data corresponding data, save that data in the database once the changes are made, and then broadcast a APPWIDGET_UPDATE
intent afterwards so that the whole update process will occur as the platform intended it to.
In another app that I have made, I didn't need any UI on click, I needed to send some information to a server through some REST API. In that case the provider was very similar, but instead of calling an activity, I was calling an IntentService
instead. The PendingIntent
construction code looked more like this:
PendingIntent pendingIntent = PendingIntent.getService(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notice the PendingIntent.getService
instead of PendingIntent.getActivity
as we have used earlier. The difference is pretty self explanatory, but the flags and parameters are exactly the same.
So basically now it's up to you to decide what's next. Depending on your requirements, hopes and dreams, figure out what is the best course of action for you, I think you have all the pieces here.
Conclusion
Developing a widget is not complicated, it's just a lot of stuff and steps to do.
I find that the official documentation is lacking a bunch of details, such as the little tips that I've been giving throughout this post: the flags for the PendingIntent
, how to communicate the configuration, how to call a service, etc.
Also, it is nice to have a proper understanding of how things are working ... it tends to remove a lot of frustrations :) So for instance, understanding the principle of broadcast receivers help with understanding the provider. Knowing about the intent mechanism also helps with the way we can communicate data across.
The full app is open source and completely available on github. Don't hesitate to clone it, fork it, do whatever you want with it.
Feel free to get back to me if I wasn't clear enough, if I made a mistake, if you found better ways of doing things, or to let me know that this was useful to you.
Thanks a bunch for reading, see you next time!