Creating an Android Collection App Widget

Photo by Max van den Oetelaar on Unsplash
Photo by Max van den Oetelaar on Unsplash
Android Collection Widgets are used to display many items of the same type on the home screen, such as picture collections, mailing lists, etc.

Android Collection Widgets are used to display many items of the same type on the home screen, such as picture collections, mailing lists, etc. So, generally speaking, collection widgets can scroll vertically to display more data. This article will introduce how to build a collection widgets.

The complete code for this chapter can be found in .

Creating a Collection Widget

First, we need to create an app widget. If you are not familiar with how to crate an app widget, please refer to the following article first. We will continue the PersonInfoAppWidget example in the following article and modify it into a collection widget.

Modify person_info_app_widget.xml as follows. Let’s change it to display a ListView. When there is no data in the ListView, it will display the TextView of No Data instead.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    style="@style/Widget.CollectionAppWidgetExample.AppWidget.Container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:theme="@style/Theme.CollectionAppWidgetExample.AppWidgetContainer">

    <ListView
        android:id="@+id/person_info_list_view"
        style="@style/Widget.CollectionAppWidgetExample.AppWidget.InnerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@null"
        android:listSelector="@android:color/transparent"
        android:scrollbarAlwaysDrawHorizontalTrack="false"
        android:scrollbars="none" />

    <TextView
        android:id="@+id/empty_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="No data"
        android:textSize="24sp"
        android:textStyle="bold|italic" />
</FrameLayout>

Added person_info_item.xml. It will be used for displaying each row in the ListView.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/name_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/job_text_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAlignment="textEnd" />

</LinearLayout>

RemoteViewsService and RemoteViewsService.RemoteViewsFactory

We need to use RemoteViewsService to provide the data to be displayed and the view of each data. In RemoteViewsService, you can get data from a databases or content providers.

Just like when using ListView, we need an Adapter to provide the view of each data. We are going to override RemoteViewsService.onGetViewFactory and return RemoteViewsService.RemoteViewsFactory. It is a thin wrapper around Adapter. Therefore, the usage is almost the same as Adapter.

class PersonInfoAppWidgetService : RemoteViewsService() {
    override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
        return PersonInfoRemoteViewFactory(this)
    }
}

class PersonInfoRemoteViewFactory(
    private val context: Context,
) : RemoteViewsService.RemoteViewsFactory {
    private lateinit var people: List<Person>

    override fun onCreate() {
        loadData()
    }

    override fun onDataSetChanged() {
        loadData()
    }

    private fun loadData() {
        people = listOf(
            Person("Wayne", "Mobile Software Developer & Blogger"),
            Person("David", "Android Developer"),
            Person("Peter", "Embedded System Developer"),
        )
    }

    override fun onDestroy() {
    }

    override fun getCount(): Int {
        print("count=${people.size}")
        return people.size
    }

    override fun getViewAt(position: Int): RemoteViews {
        return RemoteViews(context.packageName, R.layout.person_info_item).apply {
            val person = people[position]
            setTextViewText(R.id.name_text_view, person.name)
            setTextViewText(R.id.job_text_view, person.job)
        }
    }

    override fun getLoadingView(): RemoteViews? = null

    override fun getViewTypeCount(): Int = 1

    override fun getItemId(position: Int): Long = people[position].name.hashCode().toLong()

    override fun hasStableIds(): Boolean = true

}

In AppWidgetProvider.onUpdate(), we need to call RemoteViews.setRemoteAdapter() to set the adapter. RemoteViews.setEmptyView() is set to hide the view of the first parameter and display the view of the second parameter when there is no data.

class PersonInfoAppWidget : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray,
    ) {
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }
}

internal fun updateAppWidget(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int,
) {
    val intent = Intent(context, PersonInfoAppWidgetService::class.java).apply {
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
        data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
    }
    val views = RemoteViews(context.packageName, R.layout.person_info_app_widget).apply {
        setRemoteAdapter(R.id.person_info_list_view, intent)
        setEmptyView(R.id.person_info_list_view, R.id.empty_view)
    }

    appWidgetManager.updateAppWidget(appWidgetId, views)
}

Finally, because PersonInfoAppWidgetService is a Service. So, we have to declare it in AndroidManifest.xml.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application>
        <service
            android:name=".PersonInfoAppWidgetService"
            android:permission="android.permission.BIND_REMOTEVIEWS" />
    </application>
</manifest>

AppWidgetManager

So far, the PersonInfoAppWidget is roughly complete. The system will periodically call PersonInfoAppWidget.onUpdate() to update the view according to the value of updatePeriodMillis of <appwidget-provider/>. However, when the app modifies the data, the PersonInfoAppWidget cannot display the changed data immediately.

After changing the data, the app can call AppWidgetManager.notifyAppWidgetViewDataChanged() to ask the system to call PersonInfoAppWidget.onUpdate().

If the app does not know the appWidgetId of the app widget to be updated, we can use AppWidgetManager.getappWidgetIds() to obtain the appWidgetIds of all PersonInfoAppWidget instances.

AppWidgetManager.getInstance(application).let { appWidgetManager ->
    val appWidgetIds = appWidgetManager.getAppWidgetIds(
        ComponentName(application, PersonInfoAppWidget::class.java)
    )
    appWidgetManager
        .notifyAppWidgetViewDataChanged(appWidgetIds, R.id.person_info_list_view)
}

Conclusion

The collection widget seems a bit complicated, but in practice it is like implementing an Adapter. It is very similar to implementing ListView or RecycleView.

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like