I've been working on adding a widget into the app and I've hit a couple of little gotchyas. Here's what I've found and my ways around. (Hey, that rhymes.)

Bugs In The System

A widget can have a configuration activity, that is an Activity that is started when a user first drops a widget onto the home screen. This is set in the XML file where the <appwidget-provider> is defined with the android:configure option.

This Activity has a normal lifecycle (onCreate(), onResume(), etc.) and the id of the widget that you are to configure is given to you in the Intent. It can be extracted with

int widgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);

The key thing is the result code of the activity determines what happens next. If you call setResult() with a result code of RESULT_OK then your widget stays and everyone is happy. If you set the result code to RESULT_CANCELLED then the widget is removed from the home screen and the user has to start again.

In setResult() you pass in an Intent as well as a result code. In this Intent there should be an Extra with the key AppWidgetManager.EXTRA_APPWIDGET_ID and the value of your widget ID.

What isn't really explained in the widget documentation is that this should be set even when you are setting the cancelled result.

So a good way to go is to do

Intent intent = new Intent();
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
setResult(RESULT_CANCELLED, intent);

in the onCreate() of your activity. That way if the user hits Back before configuring the widget, it will be removed, and when the user has done enough to configure your widget you do

Intent intent = new Intent();
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
setResult(RESULT_OK, intent);

The eagle-eyed among you will have noticed that this section is called Bugs In The System. Where's the bug you ask? Ah, well...

It's right here.

OK, ok. The bug is marked as Released but what I found is that it exists on API16 (Jelly Bean) and not on API23 (Marshmallow). As our app's minSdk is 16 it's an issue. If your minSdk is something higher then maybe you don't need to worry about it.

In short, the bug is that if your configuration activity returns a cancelled result, your widget is left in a sort-of limbo. It exists but is not visible to the user. As you will then always have at least one widget you'll never get the onDisabled() call when the user removes the last of the visible widgets from the homescreen, and you'll get update messages for a widget that can't be seen.

There are suggestions that you don't use the configuration activity, and that your code sets a flag to remember this widget is unconfigured. Then, when you get the call to update your widget, you fire up your own activity. A grand idea. However if you don't use a configuration activity you get an update callback as soon as the user drags your widget onto the homescreen before they drop it. If you fire off your activity at this point the widget gets removed.

There doesn't seem to be any callback or notification when a widget has been dropped onto the home screen. So my solution was to keep the configuration activity, but it doesn't actually do the configuration.

protected void onCreate(Bundle savedInstanceState) {
    int widgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
    Intent resultValue = new Intent();
    resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
    setResult(Activity.RESULT_OK, resultValue);

    Intent broadcastIntent = new Intent();
    broadcastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);

What this does it to send a broadcast with the widget ID in it. Your WidgetProvider class can receive this in it's onReceive() method and then fire off the real configuration activity.

Loading Widget Images Asynchronously

The app's widget is a carousel; a 'previous item' button, an image, some text, and a 'next item' button. As the widget's view is not part of your view, you have to use a RemoteViews to manipulate it.

We use the Picasso image library by Square to load images in the rest of the app, and there is a method to load images into RemoteViews. Seems all rosy. But there's a snag.

The image loading happens asynchronously, which is good. That's what we want. But if the user hits the 'next item' button once, and then a second time before the first image has loaded and interesting effect happens.

Let's say there are three items in the carousel and the user is looking at "item 1". They then hit 'next', the text is updated to "item 2" and Picasso goes off to download 'image 2'. Before that completes the user hits 'next' again. The text is updated to show "item 3" and Picasso goes off to load 'image 3'. Then Picasso finishes loading 'image 2'. The image gets displayed and - and here's the fun bit - the text gets changed back to show "item 2". When 'image 3' finishes loading, it gets displayed and the text gets changed back to "item 3".

Why is this happening? It's mere speculation on my point, but it helps if you think of RemoteViews as being a set of instructions on how to make your widget look, i.e. set the text in this TextView, set the image in this ImageView, etc. You pass in a RemoteViews to Picasso so it knows where to put your image and in there is the instruction to set the text in the TextView. When Picasso has loaded the image it adds its instruction to set the image, and when it is processed the setting of the TextView is done as well.

The previous version of the app didn't have that issue. Why? Because the app is old, Picasso didn't exist, and the developers of the time loaded the image synchronously. This meant that the widget didn't update until the image was loaded, so this issue didn't exist. Of course what did happen was if you had no network connection (or a very slow one) your widget froze or was unresponsive.

(Note, although it looks like I'm having a go at Picasso, the same thing happened when I swapped it out for Glide, so it's not a Picasso issue.)

How to get around this? That was a little trickier. There's no callback from Picasso that I could see to notify me the image had been loaded before the widget was updated. Instead of loading the image straight into the RemoteViews, I load it into a custom Target, and when the image has loaded I check to see if the widget should show that image or has the user moved on. In the former case I set the image using the bitmap that is given to us in the Target.onBitmapLoaded() method. In the latter I just don't.

Darren @ Æ


comments powered by Disqus