Android Widgets — Let the fun begin!

Gadi Keren
Sep 2, 2018 · 6 min read
The world of mini apps is now open for your users to play with!

Learn how users can interact with your apps from their home screen!

1. Intro to Widgets
Widgets are mini apps that live on the home screen. They’re usually an extension of an existing app that’s already installed on your device. Unlike smaller launcher icons that normally appear on your home screen, widgets usually take up more space and display up-to-date information from the app that they belong to.

Most widgets are usually resizable, and the information displayed can change depending on the size of the widget.

A common widget behavior is to launch the app when clicked. Some parts of the widget can also launch a specific activity within the app.

We should remember that widgets:

(1) Widgets are a fun and easy way to interact with any app, but they could be resource intensive if not managed properly
(2) Widgets handle their own update schedule, but remember that they run on the main UI thread so to offload any heavy tasks to a new thread, probably by using IntentServices which can communicate back to the widget in any time.

2. How to create a widget
To add a widget to any application we need to create 3 things:

(1) Widget Provider — A class that extends the AppWidgetProvider class.
(2) Widget Provider Info — XML file that describes the widgets metadata, including information such as the minimum width and height, and other important properties. (Go into the XML folder)
(3) Widget Layout — XML file that describes the widget’s views which will be similar to any other layout file with some limitations. (Go into the layout folder)

Widgets communicate with the app using broadcast messages. The files above makes widgets a broadcast receiver. To make it work, we need to register the widget provider in the manifest using a <receiver> tag.

Android Studio automatically can create all of the above for us! By clicking on the “app” module and choosing “new” → “widget” → “app widget”. All settings will go into the AppWidgetProviderInfo XML. This file include resizable options, minimum width and height, and “updatePeriodMillis” which specifies in milliseconds the interval in which the widget updates itself. The shortest interval allowed is 30 minutes to prevent battery drainage. We should initialize “initalLayout” attribute to be our widget layout. In addition, we can set “previewImage” to be our launcher icon, which sets the icon in the widget menu that appears when we’re going to drag it into the home screen.

3. Customize a widget — Using RemoteViews
It’s important to note that widgets layouts are based on something called RemoteViews. This is because the widget is treated as a seperate app that runs on the home screen.

Android uses a RemoteView object to describe the view hierarchy that will be displayed in another process.

A widget’s layout seems just like any other layout XML. However, RemoteViews have some limitations on what they can support. That means not every view type can be added in a widget layout. The most popular basic views are supported, but complicated views like constraint layout or RecyclerView are not.

RemoteViews objects contain the entire view hierarchy and not just individual view items. It means, instead of finding individual views using the findViewById() method like we do for normal layouts, RemoteViews come with some special methods that allow you to directly access and update individual views inside of it (Ex: setTextViewText()).

Unfortunately, there are things that you would be able to do with a view object that just aren’t available here.

To set the click handler to launch the app we need to modify our widget provider class.

In widget provider we have the onUpdate() method, which get called once a new widget is created and in every update interval. The boilerplate loop in the method goes through all the widget instances that may have been added to the home screen and updates them (yes, there could be multiple instances of the same widget). Always assume that multiple instances do exist and we need to update them all. We do so using the AppWidgetManager class by getting access information about all existing widgets on the home screen.

To update a widget we need to pass in the id of that widget along with a RemoteViews object describing that widget.

The way RemoteViews handle click events is different than how normal views do it. We can’t simply set the onClick attribute to run a custom method as we’re used to doing.

Instead, RomoteViews can be linked to a pendingIntent that will launch once that view is clicked. This is achieved by calling the setOnClickPendingIntent() method. It takes the view id that should listen to the clicks and the pending intent.

Remember that a pendingIntent is just a wrap around an intent that allows other applications to have access and run that intent in our app.

Since Android API 14 and onwards, Android automatically includes margins between widgets as they appear on the user’s home screen. However, on previous API versions this was not the case, so it’s still a good idea to specify a margin to include around the widget on devices running Android versions less than 14.

4. Background tasks for widgets
Click events and widgets can only launch pending intents. PendingIntent doesn’t necessarily need to start an activity in the foreground. Pending intents can start activities broadcasts and services.

When we plan to something in the background launching a service will make the most sense. We can set the setOnClickPendingIntent() to a new view in our widget to start a new intent service. This intent service can run an update query in the background thread.

To do so we need to create a new intent service class which extends from IntentService, give it an action and override its onHandleIntent() method.

To make the service to send an update or response back to the widget, like updating the image, we need to do the following steps:

(1) Create a new action string in the service class
(2) Handle it at onHandleIntent() method
(3) Run a different query in the background
(4) Update the widget in the widget provide code

5. Using a GridView in a widget
We can change the layout of the widget depending on its width and height.

There are couple of collection types vies that widget can support as GridView and ListView. There’s a special collection view called StackView, which is commonly used to display and flip between photo galleries. We can also build animations using ViewFlipper.

We used AppWidgetManager to get a list of widget ids and to update any widgets layout. One of this class methods is getAppWidgetOptions() method. This method returns a bundle object containing properties associated with the widget. That’s include the widget dimensions, widget category and any custom extras that we may added to the widget.

To trigger updates of widget dimensions change (or any other widget options) we should override onAppWidgetOptionsChange() method in the widget provider and there update the widget layout.

Widget layouts are built using RemoteViews, which are slightly different than normal views and the typical layout.

The difference becomes clear when dealing with collection type views, such as ListView or GridView. Just like we need an adapter for a typical GridView, with widgets we need to create a RemoteViewsFactory, which will be basically a thin wrapper that goes around an adapter.

The typical onBindViewHolder() method in an adapter be replaced by the getViewAt() method in the RemoteViewsFactory.

On top of the RemoteViewsFactory, we will also need to create a RemoteViewsService, which is the service that connects a remote adapter to be able to request RemoteViews.

Since the RemoteViewsService is in fact a service, we also need to register it in the manifest.

We need to call the notifyAppWidgetViewDataChanged() in the AppWidgetManager, to trigger the data update to the GridView widget in order to force a data refresh.

Click events in widgets are only handled through pending intents. However, it’s very costly to set pending intents on the individual items in a collection view, so that’s not allowed.

Instead, you can set a PendingIntent template for the collection view. The PendingIntent template allows us to specify the intent that we like all the items in the collection to launch when clicked. This template is applied to each and every item in the collection. But we can customize each individual item using something called a FillInIntent.

FillInIntent allows us to pass extra data to the PendingIntent template. That way, all items in the collection will launch the same activity, but each will pass a unique extra value that will allow the launched intent to react differently.

Remember that widgets layouts are based on RemoteViews, which act a bit different than regular views:

(1) Click events are handled with pending intents and collections need a PendingIntent Template
(2) Not all view types are supported
(3) Collection views require a remote service and a remote view factory to act as an adapter

REFERENCES:

You can find much more at the free course Advanced Android App Development by Google on the Udacity website over here: (check it out!)

https://classroom.udacity.com/courses/ud855

Written by

Software Engineer | Entrepreneur | Foodie

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade