Monthly Archives: November 2014

Android: understanding Widgets

“App Widgets” section of Android documentation describes how to add a widget to your app, and does a good job at that. Unfortunately, it does not describe the generic widget concepts so well. And they are very important once you start actually doing some work.

What’s a widget?

This is actually surprisingly hard to get from various documents. Is widget a separate app? Or is it a part of the same app? But then how does the app work on home screen? Why all the hassle with RemoteViews and such?

In fact, it’s very simple.

In a sense, Android’s home screen is just another app. We do not actually run any code “in home screen context”. Instead, we register with the Home app, and give it schematics for our widget (RemoteViews). Then it creates actual Views according to these schematics. So, we do not handle actual Views, another app does.

// Create schematics
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget);
// Send to Home app
appWidgetManager.updateAppWidget(widgetId, rv);

Normally we also want to handle onClicks and other events. Usually we just register OnClickListener etc, but these only work within the same app. And Views are in another app now. So, Home app probably also registers these, but instead of running any code in the listener, it just sends us an Intent. Because Intents are basically Android’s idea of cross-process communication. Intents are also defined in the schematics.

// Create schematics
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget);
// Add intent to fire in onClick listener
Intent intent = new Intent(context, MainActivity.class);
PendingIntent pending = PendingIntent.getActivity(context, 0, intent, 
        PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.appWidgetContainer, pending);
// Send to Home app
appWidgetManager.updateAppWidget(widgetId, rv);

In other words, widget is a normal component that inflates Views and assigns Listeners to them. But the trick is, this is all done by another app. We just provide instructions.

Widgets and processes

I’ve seen people work from this concept:

Their idea was that widgets are separate entities that can run in a different process, and therefore should communicate with main app via Intents. It’s easy to make this mistake, but this point of view is wrong.

First, what’s an app, and what’s a process. An app is your project, compiled into .apk file. A process is an instance of your app running in the system. In theory, there could be multiple processes of the same app, running in parallel. In practice, Android always runs any specific app in a single process. There are rare exceptions, but you won’t have to deal with them unless you need to.

To summarize:

  • An app can either run in a specific process, or not run at all.
  • Any widget-related code will run in the same process as the main app.

As discussed above, widget code is not part of the Home app; it is part of your own app, which just provides some stuff for Home. So, it will run in your process. If your main app already runs and Home suddenly wants something from widgets, widget handlers will run in the same process already running the app. If the app isn’t running, handlers will start a new process, and that process will then be reused if you open the app.

This is very important, because this way you can share singletons, preferences, and other data between app and widgets without bothering with Intents and data synchronization between processes.

Lifecycle of a widget

Note on onUpdate (1). Documentation states that inf you have a configuration activity, onUpdate will not be called before the activity produces any result. At the time of this writing, it’s a lie. It is in fact called regardless of configuration activity, which will cause an unconfigured widget update in your code. To avoid that, people store a flag in SharedPreferences and set it once configuration activity is about to be done.

public class Widget extends AppWidgetProvider {
  
    public void onEnabled(Context context) {
        // First instance of this widget was created
        super.onEnabled(context);
    }
  
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, 
            int[] appWidgetIds) {
        // System wants update on widget(s), on startup, or after creating a new one
        if (!prefs.getBoolean("configured-" + widgetId, false))
            return; // not configured yet
        for (int widgetId : appWidgetIds) 
            update(context, appWidgetManager, widgetId);
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }
  
    public void update(Context context, AppWidgetManager appWidgetManager, 
            int widgetId) {
        // If you use configuration activity, call this manually after it's done
        SharedPreferences prefs = 
           PreferenceManager.getDefaultSharedPreferences(context);
        if (!prefs.getBoolean("configured-" + widgetId, false))
            return; // not configured yet
        // Send RemoteViews here
    }
  
    public void onDeleted(Context context, int[] appWidgetIds) {
        // User deleted widget(s)
        for (int widgetId : appWidgetIds) {
        }
        super.onDeleted(context, appWidgetIds);
    }
  
    public void onDisabled(Context context) {
        // Last instance of this widget was deleted
        super.onDisabled(context);
    }
}

Naming conventions

This is the part which doesn’t seem at all important at first, but can easily become an annoyance later. Let’s say you got some kind of sample widget working, and are now starting to apply it to your project. It’s almost guaranteed that entire code of the sample is in its main package (such as “com.example.mytest.MyWidget”). Since there’s only a couple of classes, your natural move will be to just drop them in the main package of your project as well (such as “com.acme.explosives.MyWidget”), and maybe even leave it as “MyWidget”, because what’s the diff.

Don’t be lazy. Take 5 minutes to think of a good name for your class, and create a dedicated package for it. For example, create it as “com.acme.explosives.widgets.TNTWidget” instead.

Reason for that is, once you deployed your app, your widget class is pretty much set in stone. When customers add your widgets to home page, system will record their class names, and will not give you a chance to change them later. So, you will not be able to either rename the widget class, or move it to another package, unless you’re willing to break widgets for every customer and seriously piss off your managers. You’ve named your widget “TestWidget”? Congratulations, you’re stuck with this name forever. You’ve eventually created 5 more widgets, and they require like 15 support classes? All that stuff will forever clutter your main package, you won’t be able to isolate it. I’ve seen both of these things happen on more than one project.

So, again:

  • pick a good name,
  • create “widgets” package right away.

Updating widget with service

A widget can be periodically updated on intervals defined by updatePeriodMillis. However, minimum interval is 30 minutes. Usually people are greedy and want instant updates. So, at least every 5 minutes.

This kind of requirement is implemented with a service handled by AlarmManager.

public class Widget extends AppWidgetProvider {

    public void update(Context context, AppWidgetManager appWidgetManager, 
        int widgetId) {
        // Do checks from previous example
        ...
        // If you don't have updatePeriodMillis, this will be called once on 
        // startup or widget creation. So, start alarm on this first call.
        setAlarm(context, widgetId, 5 * 60 * 1000);
    }
  
    public void onDeleted(Context context, int[] appWidgetIds) {
        for (int widgetId : appWidgetIds) {
            // Remove alarm
            setAlarm(context, widgetId, -1);
        }
        super.onDeleted(context, appWidgetIds);
    }
  
    public void setAlarm(Context context, int widgetId, long interval) {
        Intent intent = new Intent(context, WidgetUpdateService.class);
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
        PendingIntent pendingIntent = PendingIntent.getService(context, 
            widgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        
        AlarmManager am = (AlarmManager) context.getSystemService(
            Context.ALARM_SERVICE);
        am.cancel(pendingIntent);
        if (interval >= 0) 
            am.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), 
                interval, pendingIntent);
    }
}

public class WidgetUpdateService extends Service {

    public void onStart(Intent intent, int startid) {
        int widgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
        // Process new data
        ...
        // Send updated widget schematics to Home app
        if (haveNewData) {
            AppWidgetManager awm = AppWidgetManager.getInstance(this);
            RemoteViews rv = new RemoteViews(context.getPackageName(), 
                R.layout.widget);
            awm.updateAppWidget(widgetId, rv);
        }
        // Stop service
        stopSelf();
    }
}

The service runs in main thread. So, if you need to download something, make sure to start another thread in the service.

Another very important point which many people don’t care about, until it bites them in the ass. Notice the stopSelf() call at the end. You must do this call when the service is done. It can be difficult, because if you have multiple things to download, you have to predict all possible cases of this service finishing all threads. Like, case 1: nothing to download, stop now; case 2: download thread completed, stop now; case 3: there was an error, stop now. See how annoying this is already? That’s why nobody likes doing it, and you won’t find this call in many samples.

What will happen if you don’t include stopSelf() call, is everything will look normal, but the system won’t actually stop your app. It will be present in Settings – Apps – Running, and will indicate that the app runs this service. For all I know (I didn’t actually check), the system runs CPU cycles for the app the entire time, wasting battery. That’s why it’s very important to always make this call. It’s also a good practice to check if your service calls onDestroy() every time after it’s done.

Advertisements