Killer Way to Show a List of Items in Android Collection Widget

Share this article

Killer Way to Show a List of Items in Android Collection Widget

In the earlier versions of Android, app widgets could only display views like TextView, ImageView etc. But what if we want to show a list of items in our widget? For example, showing the list of temperature information for the whole next week. Collection widgets were introduced in Android 3.0 to provide this additional benefit. Collection widgets support ListView, GridView and StackView layouts.

Today, I am going to help you understand how the collection widget works. We are going to build an app widget for a Todo app. Collection widgets will be used to display the list of pending tasks.

I assume you already know how to make a basic app widget. If not please refer to this article and come back when you are ready to build your own collection widgets.

Getting Started

Please download the starter project code here as we’ll build from it.

The code has a basic widget already implemented, we are going to create a collection widget within the same project. A basic widget shows the number of pending tasks and collection widget will show the complete list. To make a collection widget, two main components are required in addition to the basic components:

  • RemoteViewsService
  • RemoteViewsFactory

Let’s understand what these components do.

Using RemoteViewsFactory

RemoteViewsFactory serves the purpose of an adapter in the widget’s context. An adapter is used to connect the collection items(for example, ListView items or GridView items) with the data set. Let’s add this class into our project. Create a new Java class, name it MyWidgetRemoteViewsFactory, and set it to implement the class RemoteViewsService.RemoteViewsFactory.

public class MyWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

    private Context mContext;
    private Cursor mCursor;

    public MyWidgetRemoteViewsFactory(Context applicationContext, Intent intent) {
        mContext = applicationContext;
    }

    @Override
    public void onCreate() {

    }

    @Override
    public void onDataSetChanged() {

        if (mCursor != null) {
            mCursor.close();
        }

        final long identityToken = Binder.clearCallingIdentity();
        Uri uri = Contract.PATH_TODOS_URI;
        mCursor = mContext.getContentResolver().query(uri,
                null,
                null,
                null,
                Contract._ID + " DESC");

        Binder.restoreCallingIdentity(identityToken);

    }

    @Override
    public void onDestroy() {
        if (mCursor != null) {
            mCursor.close();
        }
    }

    @Override
    public int getCount() {
        return mCursor == null ? 0 : mCursor.getCount();
    }

    @Override
    public RemoteViews getViewAt(int position) {
        if (position == AdapterView.INVALID_POSITION ||
                mCursor == null || !mCursor.moveToPosition(position)) {
            return null;
        }

        RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.collection_widget_list_item);
        rv.setTextViewText(R.id.widgetItemTaskNameLabel, mCursor.getString(1));

        return rv;
    }

    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    @Override
    public int getViewTypeCount() {
        return 1;
    }

    @Override
    public long getItemId(int position) {
        return mCursor.moveToPosition(position) ? mCursor.getLong(0) : position;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

}

In the code above, MyWidgetRemoteViewsFactory overrides a few methods from the RemoteViewsFactory class:

  • onCreate is called when the appwidget is created for the first time.
  • onDataSetChanged is called whenever the appwidget is updated.
  • getCount returns the number of records in the cursor. (In our case, the number of task items that need to be displayed in the app widget)
  • getViewAt handles all the processing work. It returns a RemoteViews object which in our case is the single list item.
  • getViewTypeCount returns the number of types of views we have in ListView. In our case, we have same view types in each ListView item so we return 1 there.

Using RemoteViewsService

The main purpose of RemoteViewsService is to return a RemoteViewsFactory object which further handles the task of filling the widget with appropriate data. There isn’t much going on in this class.

Create a new class named MyWidgetRemoteViewsService extending the class RemoteViewsService.

public class MyWidgetRemoteViewsService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new MyWidgetRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

As with all the other services in android, we must register this service in the manifest file.

<service android:name=".AppWidget.MyWidgetRemoteViewsService"
	android:permission="android.permission.BIND_REMOTEVIEWS"></service>

Note the special permission android.permission.BIND_REMOTEVIEWS. This lets the system bind your service to create the widget views for each row and prevents other apps from accessing your widget’s data.

Starting the RemoteViewsService

Now that we have the additional components set up, it’s time to create the WidgetProvider to call the RemoteViewsService.

Create a new class in AppWidget package and name it CollectionAppWidgetProvider:

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
        RemoteViews views = new RemoteViews(
                context.getPackageName(),
                R.layout.collection_widget
        );
        Intent intent = new Intent(context, MyWidgetRemoteViewsService.class);
        views.setRemoteAdapter(R.id.widgetListView, intent);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
}

Creating the Widget Layout

Now create a new resource file in res/xml and name it collection_widget.xml.

In this file, we define the widget settings, such as which layout file the widget should be using, and adding a preview image for better user experience.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="864000"
    android:previewImage="@drawable/simple_widget_preview"
    android:initialLayout="@layout/collection_widget"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

Create one more resource file but this time in res/layout and name it collection_widget.xml In this file, we define the layout of what we want to show in our collection widget. We are going to have a title on top and then the ListView in the bottom to display the list of tasks.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorWhite"
            xmlns:tools="http://schemas.android.com/tools"
            android:orientation="vertical">
    <FrameLayout android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView android:layout_width="match_parent"
            android:id="@+id/widgetTitleLabel"
            android:text="@string/title_collection_widget"
            android:textColor="@color/colorWhite"
            android:background="@color/colorPrimary"
            android:textSize="18dp"
            android:gravity="center"
            android:textAllCaps="true"
            android:layout_height="@dimen/widget_title_min_height"></TextView>
    </FrameLayout>
    <LinearLayout android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ListView android:id="@+id/widgetListView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorWhite"
            android:dividerHeight="1dp"
            android:divider="#eeeeee"
            tools:listitem="@layout/collection_widget_list_item"></ListView>
    </LinearLayout>
</LinearLayout>

We need to create one more file in res/layout to define the layout of each list item.

Create this file and name it collection_widget_list_item.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:paddingLeft="@dimen/widget_listview_padding_x"
    android:paddingRight="@dimen/widget_listview_padding_x"
    android:paddingStart="@dimen/widget_listview_padding_x"
    android:paddingEnd="@dimen/widget_listview_padding_x"
    android:minHeight="@dimen/widget_listview_item_height"
    android:weightSum="2"
    android:id="@+id/widgetItemContainer"
    android:layout_height="wrap_content">

    <TextView android:id="@+id/widgetItemTaskNameLabel"
        android:layout_width="wrap_content"
        android:gravity="start"
        android:layout_weight="1"
        android:textColor="@color/text"
        android:layout_gravity="center_vertical"
        android:layout_height="wrap_content"></TextView>

</LinearLayout>

Run the app now, you should be able to see the widget populated with todo items. (Make sure you re-install the app to see the changes. You can also disable the Instant Run option in Android Studio).

Updating the widget manually

The logic goes like this: whenever you create a new todo item, you have to send a Broadcast to WidgetProvider. Define a new method in CollectionAppWidgetProvider class.

public static void sendRefreshBroadcast(Context context) {
    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    intent.setComponent(new ComponentName(context, CollectionAppWidgetProvider.class));
    context.sendBroadcast(intent);
}

then override the onReceive method in the CollectionAppWidgetProvider class,

@Override
public void onReceive(final Context context, Intent intent) {
    final String action = intent.getAction();
    if (action.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) {
        // refresh all your widgets
        AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        ComponentName cn = new ComponentName(context, CollectionAppWidgetProvider.class);
        mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.widgetListView);
    }
    super.onReceive(context, intent);
}

When the new todo task is created, call the sendRefreshBroadcast method defined in CollectionAppWidgetProvider class.

In MainActivity, modify the addTodoItem method accordingly.

a.runOnUiThread(new Runnable() {
    @Override
    public void run() {
        Toast.makeText(mContext, "New task created", Toast.LENGTH_LONG).show();
        getTodoList();
        // this will send the broadcast to update the appwidget
        CollectionAppWidgetProvider.sendRefreshBroadcast(mContext);
    }
});

Event handling in widgets

In our widget, we have a title on the top and a list view in the bottom. So when the user clicks on the title, we launch the app. When a single item is clicked in the list view, we launch the details activity. In our todo app, detail activity may not be that useful, but let’s just do it to understand the concept.

Click event on single views

Adding click events to views like TextView, ImageView, etc is very easy. Here is the updated code for the onUpdate method.

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
        RemoteViews views = new RemoteViews(
                context.getPackageName(),
                R.layout.collection_widget
        );

        // click event handler for the title, launches the app when the user clicks on title
        Intent titleIntent = new Intent(context, MainActivity.class);
        PendingIntent titlePendingIntent = PendingIntent.getActivity(context, 0, titleIntent, 0);
        views.setOnClickPendingIntent(R.id.widgetTitleLabel, titlePendingIntent);

        Intent intent = new Intent(context, MyWidgetRemoteViewsService.class);
        views.setRemoteAdapter(R.id.widgetListView, intent);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
}

The idea here is similar to how we add click events in our apps. But as widgets run in a different context, we need to register the click event through a PendingIntent.

Click events on ListView items

Adding click events on ListView items is not as simple as setting up the setOnItemClickListener on the ListView object. It requires some additional steps.

First you need to setup a template for PendingIntent. Add this code in onUpdate method in CollectionAppWidgetProvider class after views.setRemoteAdapter(R.id.widgetListView, intent);

// template to handle the click listener for each item
Intent clickIntentTemplate = new Intent(context, DetailsActivity.class);
PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
        .addNextIntentWithParentStack(clickIntentTemplate)
        .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.widgetListView, clickPendingIntentTemplate);

For each ListView item we are launching DetailsActivity which will simply display the task description sent as an extra.

Then fill this template every time a new RemoteViews object is created by the RemoteViewsFactory. Add this code in getViewAt method in MyWidgetRemoteViewsFactory class.

Intent fillInIntent = new Intent();
fillInIntent.putExtra(CollectionAppWidgetProvider.EXTRA_LABEL, mCursor.getString(1));
rv.setOnClickFillInIntent(R.id.widgetItemContainer, fillInIntent);

Here we are filling the pending intents template defined in CollectionAppWidgetProvider class. Note that we want to make the full row clickable, so we are setting the click listener on the root element of collection_widget_list_item.xml

Conclusion

In this article, I tried to help with the most common issues that beginners usually face. If you have any questions or if anything doesn’t work for you, let me know in comments below.

You can download the full working code here.

Frequently Asked Questions (FAQs) about Android Collection Widget

How can I customize the appearance of my Android Collection Widget?

Customizing the appearance of your Android Collection Widget involves modifying the XML layout file associated with the widget. You can change the background color, text color, font size, and other attributes by editing this file. For instance, to change the background color, you can use the “android:background” attribute and specify the color you want. Remember to always test your changes on different devices and screen sizes to ensure your widget looks good across all possible scenarios.

What is the role of an Adapter in an Android Collection Widget?

An Adapter acts as a bridge between the data source and the widget. It retrieves data from the source, converts each data entry into a view that can be added to the widget. The Adapter also updates the widget when the underlying data changes. It’s a crucial component in the functioning of any Android Collection Widget.

How can I handle user interactions with my Android Collection Widget?

User interactions with your Android Collection Widget can be handled using PendingIntent. PendingIntent allows the application to perform an action as if it was performed by another application with certain permissions. You can use it to open an activity, service, or broadcast receiver. For example, if you want to open an activity when a user clicks on an item in the widget, you can create a PendingIntent for the activity and set it on the item’s view.

How can I update the data in my Android Collection Widget?

To update the data in your Android Collection Widget, you need to notify the adapter that the underlying data has changed. This can be done by calling the notifyDataSetChanged() method on the adapter. Once this method is called, the adapter will refresh the views in the widget to reflect the new data.

How can I add animations to my Android Collection Widget?

Animations can be added to your Android Collection Widget by using the ViewAnimator class. This class provides methods to animate the transition between views. You can specify the animation to use when switching between views by calling the setInAnimation() and setOutAnimation() methods.

How can I optimize the performance of my Android Collection Widget?

Performance of your Android Collection Widget can be optimized by implementing view recycling in your adapter. View recycling reuses old views that are no longer visible instead of creating new ones, reducing memory usage and improving scrolling performance. This can be done by overriding the getView() method in your adapter and checking if the convertView parameter is null before creating a new view.

How can I add a header or footer to my Android Collection Widget?

Headers or footers can be added to your Android Collection Widget by using the addHeaderView() and addFooterView() methods of the ListView class. These methods take a view object which will be used as the header or footer. Note that these views are not selectable and do not have click events.

How can I filter the data in my Android Collection Widget?

Data in your Android Collection Widget can be filtered by implementing the Filterable interface in your adapter. This interface provides a getFilter() method which returns a Filter object. You can use this object to perform filtering operations on your data.

How can I handle empty states in my Android Collection Widget?

Empty states in your Android Collection Widget can be handled by using the setEmptyView() method of the ListView class. This method takes a view object which will be displayed when the adapter has no data. This is useful for displaying a message or an image when the widget is empty.

How can I test my Android Collection Widget?

Testing your Android Collection Widget can be done by using the Android testing framework. This framework provides tools and APIs to simulate user interactions, check the state of the widget, and verify the results. You can write unit tests for your adapter and integration tests for your widget.

Gagandeep SinghGagandeep Singh
View Author

Gagandeep has been a freelance developer for over five years. He is passionate about developing Android apps, and spends most of his extra time thinking of new ideas and Android techniques. He is currently studying in Computer Applications at Guru Nanak Dev University, Jalandhar, India.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week