Developing an Android home screen app widget is not for the faint of heart. The official Android widget documentation is sketchy and sometimes just plain wrong. Following instructions online I had trouble creating a widget that had a configuration button on it and used an AlarmManager to update the widget in real time. The problem was that the widget would randomly become unresponsive. After large doses of caffeine and Stack Overflow, I finally figured out what was wrong. I will demonstrate the problem and its solution in this post. You can download the code for the sample widget here and try it yourself using Android Studio with the Android SDK, or just follow along with the code snippets given below.
The Widget
The example widget is shown below. It has two TextViews with a button in the middle. The top TextView is the user configurable part of the widget, in this case the user name. The button is used to change the user name in the top TextView. The bottom TextView shows the current date and time and is updated every second.
Widget Design Considerations
Android widgets in their standard form can only be updated at a minimum of every 30 minutes, so our example widget gets around this by using an AlarmManager that triggers every second. We won’t go into detail about how to do this but we will refer you to this tutorial if you are interested. We get the current time from a Service that is included in the AndroidManifest and is thus started when the widget is deployed. Although a Service is probably not necessary in such a simple example, if your updates to your widget take any time (such as getting data from the Internet) use of a Service is necessary to prevent Application Not Responding (ANR) messages.
Another decision we need to make is how to set up our configuration activity. Android widgets have a mechanism that allows launching of the configuration activity when the widget is first started. According to the documents, you can return a code saying the widget launch was cancelled if the back button is pushed and the widget will never launch. It also says in the documents that the onUpdate method of the widget is not called by the configuration activity when it is launched and that it is important to manually trigger a widget update when you close your configuration activity. All these statements are wrong. Using Log statements to document the widget lifecycle, onUpdate is called when the configuration activity is launched. Also, up until Android API level 18 (it is fixed in level 19), the widget is not deleted when you back out of the configuration activity, thus leaving you with an invisible widget that is taking up resources — what I call a zombie widget. This is particularly bad if we do something in the onUpdate method of the widget, such as starting an AlarmManager, because there is no way to stop this alarm if the widget is invisible (short of uninstalling the widget). There are ways around these problems, but they are too complex for our simple example. So we will forego using the Android built-in configuration activity process and instead launch our widget unconfigured. The user will need to press the button to configure the widget, but at least this approach eliminates the zombies.
Creating the Example Widget
Take a look at the AndroidManifest. The first part of the application is not an Activity, but a Receiver. The AppWidgetProvider class is a subclass of BroadcastReceiver. We need an intent-filter to catch APPWIDGET_UPDATE events. This is not to allow timed updates of our widget, but to allow our Configuration class to notify our widget to update its user name TextView. We add the Configuration class as a plain Activity (it will not be called when the widget starts), and add our Service (ClockService).
<receiver android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name= "android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget /> </receiver> <activity android:name=".Configure" </activity> <service android:name=".ClockService" > </service>
Each widget has a widget.xml file that shows its size and other features. If we were using our Configuration class as a typical Android configuration Activity we would indicate this here (we’re not). Since we are not updating our widget based on time, we set the update interval in res/xml/widget.xml to 0 (= never).
android:updatePeriodMillis="0"
Our Configure class updates the user name. It stores it in SharedPreferences, which is the way we need to communicate with our widget. Since multiple widgets can be present on the home screen, we add the appWidgetId to the key for the SharedPreferences, so that each widget can store its own values. Obviously you could put in a much more complex configuration here, but this is an example. The OK button of the Configure activity sends an update to our main widget which tells it to look at SharedPreferences and get the user name. Here is part of the code in Configure.java:
Button ok = (Button) findViewById(R.id.ok_button); ok.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { saveUserName(context, appWidgetId, labelEditText.getText() .toString()); // We need to broadcast an APPWIDGET_UPDATE to our appWidget // so it will update the user name TextView. AppWidgetManager appWidgetManager = AppWidgetManager .getInstance(context); ComponentName thisAppWidget = new ComponentName(context .getPackageName(), MainActivity.class.getName()); Intent updateIntent = new Intent(context, MainActivity.class); int[] appWidgetIds = appWidgetManager .getAppWidgetIds(thisAppWidget); updateIntent.setAction("android.appwidget.action. APPWIDGET_UPDATE"); updateIntent.putExtra(AppWidgetManager. EXTRA_APPWIDGET_IDS, appWidgetIds); context.sendBroadcast(updateIntent); // Done with Configure, finish Activity. finish(); } });
And here is our simple configuration activity screen:
Our original version of the ClockService just updates the date and time TextView when called. It seemed to me that having the ClockService just update that TextView and nothing else was the most efficient implementation. Nothing else in the widget changes with the clock, and the other parts of the widget are only changed by the Configuration activity. Here is the relevant code. Please ignore the commented out code for now.
public int onStartCommand(Intent intent, int flags, int startId) { Context context = getApplicationContext(); int appWidgetId = intent.getExtras().getInt( AppWidgetManager.EXTRA_APPWIDGET_ID); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.activity_main); AppWidgetManager appWidgetManager = AppWidgetManager .getInstance(context); Format formatter = new SimpleDateFormat( "EEEE, MMMM d yyyy\nhh:mm:ss a z", Locale.getDefault()); String currentTime = formatter.format(new Date()); views.setTextViewText(R.id.time_label, currentTime); // Ignore commented out code for now. appWidgetManager.updateAppWidget(appWidgetId, views); return START_STICKY; }
The onUpdate method of the MainActivity updates the user name TextView. The button is set up with a PendingIntent to call the Configure activity. The AlarmManager starts a clock that calls our ClockService once a second to update the date and time TextView. (We don’t show the setAlarm code here, but it is in the source code referenced above if you are curious). To make sure the Alarm stops we have code to stop the alarm in the onDeleted method of the widget.
public void onUpdate( Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { Log.d(LOG_TAG, "onUpdate"); for (int appWidgetId : appWidgetIds) { // onUpdate is only called when the widget starts, // since update interval is set to 0. // Our Configure activity also calls it when its // OK button is touched. // We'll start the alarm service to update the // Service once a sec. setAlarm(context, appWidgetId, 1000); // 1000 msec Log.d(LOG_TAG, "Alarm started"); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.activity_main); // we'll set up a PendingIntent to open //the Configure activity // when our button is touched. Intent intent = new Intent(context, Configure.class); intent.putExtra(AppWidgetManager. EXTRA_APPWIDGET_ID, appWidgetId); PendingIntent pendingIntent = PendingIntent. getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent( R.id.update_button, pendingIntent); // Now we'll update the time, but //all future updates will be by our // ClockService Format formatter = new SimpleDateFormat( "EEEE, MMMM d yyyy\nhh:mm:ss a z", Locale.getDefault()); String currentTime = formatter.format(new Date()); views.setTextViewText(R.id.time_label, currentTime); // And the label String userName = Configure.loadUserName( context, appWidgetId); if (userName != null) { views.setTextViewText(R.id.user_name_label, userName); } appWidgetManager.updateAppWidget(appWidgetId, views); } super.onUpdate(context, appWidgetManager, appWidgetIds); }
If you run the app and install it on a device or use the emulator, it seems to work. The problem is the button can become unresponsive after a while, and the user name TextView changes back to its default label. This behavior can be reproduced consistently by rotating the screen and is shown below. After rotating the screen the button doesn’t work anymore, even though the time continues to update without any problem. Click on the two images below to enlarge them and see the problem.
So what is happening? Why can’t I just update the user name TextView in onUpdate and not worry about it changing back to its default with rotation? Why isn’t the PendingIntent attached to the button surviving screen rotation? The problem seems to be that when the widget views are updated in ClockService by writing to the date-time TextView, the other customizable parts of the widget, including the user name TextView and the PendingIntent attached to the button, are forgotten. Forcing the view to redraw itself by rotating the screen brings this problem out immediately, but even without screen rotation, on a real device that sleeps and wakes and restarts this problem eventually becomes apparent. The widget becomes unresponsive. Ugh!
The Fix
To fix this, it is necessary to understand that whenever any part of the widget is updated, the entire widget must be updated. To do this, uncomment the code in ClockService. This code is a repeat of the code in onUpdate. So every clock tick, the PendingIntent is sent to the button, and the user name is rewritten to the TextView. Seems wasteful but necessary. Here is the code added to ClockService.java:
public int onStartCommand(Intent intent, int flags, int startId) { // skipped code........... views.setTextViewText(R.id.time_label, currentTime); // added code String userName = Configure.loadUserName(context, appWidgetId); if (userName != null) { views.setTextViewText(R.id.user_name_label, userName); } Intent configureIntent = new Intent(context, Configure.class); configureIntent.putExtra(AppWidgetManager. EXTRA_APPWIDGET_ID, appWidgetId); PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, configureIntent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.update_button, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); return START_STICKY; }
And here we see that rotation works.
Another Fix
Thanks to Trevin Avery for his comment below on another, cleaner solution to the rotation problem. Instead of using updateAppWidget(), there is, starting with Android version 11, a partiallyUpdateAppWidget() method. This does exactly what we want, that is, just updates the views that have changed. With this method, it is not necessary to update the label or remake the PendingIntent every time the widget is updated. In the source code, remove the code added above and change
appWidgetManager.updateAppWidget(appWidgetId, views);
to
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views);
With that, only the date and time will be updated. The remainder of the views (e.g. the label and button) will not be affected, and rotating the screen which redraws the widget will not affect the appearance of the widget. As long as you don’t have to support Android versions < 11, this is probably the very best solution to the rotation problem.
In Conclusion
Android widget programming remains difficult. Thank goodness for StackOverflow and the programmers who have worked though these problems and found solutions. I hope by sharing how I solved this particular problem I am giving something back to those who have helped me. If you have any questions about this example, leave comments or email me (mannd<at>epstudiossoftware.com).
UPDATE: please see comments below on using partiallyUpdateAppWidget() which is a more efficient way to solve this problem.
UPDATE #2: The comments indicate that it is necessary to import the appcompat_v7 library to run the widget. The source code has been modified since then so it is NOT necessary to link to appcompat_v7 library. The code should be downloaded from GitHub, loaded into Eclipse Android Studio and then can be compiled and run without any further modification.
UPDATE #3: Post modified to add the partiallyUpdateAppWidget() method. Source code on GitHub also has been modified to correspond with the text.
UPDATE #4: Code modified to run with Android Studio. Bug fix applied so that correct widget gets updated when multiple widgets used (see comment of Matteo Zandi).
Hi…
Thanks for the article. I did try to import your project into Eclipse but unfortunately it would not compile even after closing and re-opening Eclipse. The issue seemed to be with the R.id..
I could not easily figure out the root cause and since I’m neck deep in my own widget woes I thought I would just mention it. You may wish to try importing your own github into Eclipse and seeing if it works out of the box so to speak.
I have derived value from your explanation and some bit of comfort knowing that I am not alone in cursing the Android gods for doing such a poor job of documenting such an important feature.
You need to add the AppCompat library as described in the answer to this StackOverflow question: http://stackoverflow.com/questions/21059612/no-resource-found-that-matches-the-given-name-style-theme-appcompat-light
I imported the project from github and it compiled once I added the appcompat library as described above.
In your service you call
appWidgetManager.updateAppWidget(appWidgetId, views);
to update the time label on your widget. This replaces the existing layout with “views”, which is what causes your name label and pendingIntent to be reset to default. However, if you instead call
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views);
this will only replace the views that you have changed. This way you wont need to reset the name label or remake the pendingIntent every time you update the time label. Instead just update them as you did before, in “onUpdate()”, which may be more efficient.
Wow! I guess it speaks to the state of Android widget documentation that I was unaware of the existence of partiallyUpdateAppWidget(). Thanks so much for this suggestion. I will rewrite the demo app and test this myself, then update the post. Again, many thanks.
Note though that partiallyUpdateAppWidget() is only available in Android version 11 and up. If you are using earlier versions, the workaround in the post is necessary.
Although this contains good information, this is not a tutorial. A tutorial describes how to accomplish something in a step-by-step manner. This is just an overview with some sample code.
Point taken.
Hello,
line 53 of your main activity is:
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
but should read:
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
in this way preferences are correctly saved for each instance of your appWidget.
Thanks for the article
Matteo
I updated the post and the source code on GitHub. I must say the requestCode parameter of PendingIntent.getActivity() is not very well documented! Thanks for helping me learn.