Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
Written by Nikos Vaggalis   
Monday, 03 July 2017
Article Index
Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
Step 1 Fragments
Step 2 - Libraries & Networking
Step 3 - Adding Exoplayer
Step 4 - Widgets
Step 5 - The Widget Provider
Step 6 - UI Testing
Step 7 - Testing Intents


Step 4 - Enhancing the user experience with home screen Widgets

Widgets, despite being an extension of an existing app that's already installed in the user's device, are treated as separate applications that live on the home screen and at a glance offer up-to-date information from the 'parent' application.In this case, the Baking App widget should display the ingredient list for the desired recipe.As simplistic as it sounds, in reality it's a tedious process that involves WidgetProviders, RemoteViews, Services and Broadcast receivers and more.

Let's take it step by step, first looking at the official definition of the AppWidgetProviderInfo:

"AppWidgetProviderInfo describes the metadata for an App Widget, such as the App Widget's layout, update frequency, and the AppWidgetProvider class. Define the AppWidgetProviderInfo object in an XML resource using a single <appwidget-provider> element and save it in the project's res/xml/ folder"

image18a

 

The one for Baking App is stored in 'baking_widget_info.xml'.This file contains all the necessary information in setting up the widget's initial appearance according to the following specifications:

   
android:initialLayout="@layout/baking_widget_before_recipe"
android:previewImage=
                    "@drawable/ic_chrome_reader_mode_black_48dp"


The AppWidgetProviderInfo also sets the interval that governs how often the widget gets auto-updated by means of a system-wide timer 

    android:updatePeriodMillis="1800000"

The layout file, 'layout/baking_widget_before_recipe.xml'


/****
layout/baking_widget_before_recipe.xml
****/

 <RelativeLayout
    xmlns:android=
           "http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    android:background=
                 "@drawable/ic_chrome_reader_mode_black_48dp"
    android:textAlignment="viewStart">

            <TextView
                android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:contentDescription="@string/appwidget_text"
                        android:text="@string/appwidget_text"
                        android:textColor="#ffffff"
                        android:textSize="14sp"
                        android:fillViewport="false"
                        android:textStyle="bold|italic"
                android:layout_alignParentRight="false"
                android:layout_alignParentBottom="false"
                android:layout_centerInParent="false"
                android:layout_centerHorizontal="false"
                android:layout_centerVertical="false"
                android:layout_alignParentTop="false"
                android:layout_alignParentStart="true" />

</RelativeLayout>


sets the widget's thumbnail image to that of a recipe book icon 'ic_chrome_reader_mode_black_48dp' and accompanies it with the text "RECIPE INGREDIENTS WILL APPEAR HERE AFTER LAUNCHING BAKING APP".

There are sill two more layouts, 'layout/widget_grid_view.xml' for the GridView with the list of a recipe's ingredients:


/****
 layout/widget_grid_view.xml
****/

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <GridView
        android:id="@+id/widget_grid_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:numColumns="1"
        android:columnWidth="25dp"
        android:layout_margin="2dp"
        android:horizontalSpacing="5dp"
        android:verticalSpacing="5dp"
         />

</FrameLayout>


and 'layout/widget_grid_view_item.xml' for the individual TextView items directly mapped to each GridView's cell:


/****
 'layout/widget_grid_view_item.xml'
****/

<?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="match_parent"
    android:background="#09C"
    android:orientation="vertical">


    <TextView
        android:id="@+id/widget_grid_view_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAlignment="viewStart"
        android:textStyle="bold"
        android:textSize="10sp"
        android:layout_margin="3dp"
         />

</LinearLayout>


Each one of these TextView elements will contain the ingredient's title concatenated with its required quantity and measure.


It's important to note that those two widget layouts are based on something called RemoteViews and RemoteViews  have some limitations on what kind of layouts they can support. A RemoteViews object and, consequently a widget, support the following layout classes:

  •     FrameLayout
  •     LinearLayout
  •     RelativeLayout
  •     GridLayout


As you can see the most popular views are indeed supported, but others like Constraintlayout or Recyclerviewer are not.

The next component to look at in a Widget's hierarchy is the AppWidgetProvider. A widget is in fact a BroadcastReceiver, i.e it listens for events taking place in the Android OS and when it notices that one carries a message destined for it, it captures that message's associated Intent and works on it. The AppWidgetProvider acts as a convenience class that replaces the BroadcastReceiver, since everything you can do with a BroadcastReceiver, you can do with an AppWidgetProvider plus it lets you parse the relevant fields out of the Intent received in its onReceive(Context,Intent) callback.

The AppWidgetProvider also defines the basic methods that allow for programmatic handling of the widget's states of getting updated, enabled, disabled or deleted. We are expected to override one or more related callbacks,

onUpdate(Context, AppWidgetManager, int[]),

onDeleted(Context, int[]), onEnabled(Context)

or

onDisabled(Context)

to implement our own AppWidget's functionality.


First, as with all BroadcastReceivers, we have to declare the AppWidgetProvider in our application's AndroidManifest.xml file.

 
   <receiver
         android:name=".widget.BakingWidgetProvider"          
         android:icon="@drawable/ic_art_track_black_36dp">
            <intent-filter>
                <action
                      android:name=
                         "android.appwidget.action.APPWIDGET_UPDATE" />
                <action
                     android:name=
                         "android.appwidget.action.APPWIDGET_UPDATE2" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/baking_widget_info" />
    </receiver>
       


The <receiver> element requires the 'android:name attribute', which should point to the specific subtype of AppWidgetProvider used by our app's Widget, namely the  BakingWidgetProvider.

The <intent-filter> element must include an <action> element with the 'android:name attribute' which specifies that the AppWidgetProvider accepts the ACTION_APPWIDGET_UPDATE broadcast. This is the only broadcast we must explicitly declare, as the system-wide AppWidgetManager automatically forwards all other Widget broadcasts to the correct AppWidgetProvider as necessary.

The <meta-data> element specifies the AppWidgetProviderInfo resource and requires the following attributes:

android:name - specifies the metadata name

android:resource - specifies the AppWidgetProviderInfo resource location

What this all means is that when our BakingWidgetProvider
receives a broadcasted message with the

"android.appwidget.action.APPWIDGET_UPDATE2"

action, a custom action that deviates from the default

"android.appwidget.action.APPWIDGET_UPDATE",

is in fact notified that there was an update in the parent app and that there's also an associated bundle (the list of ingredients) attached to it. In turn the BakingWidgetProvider extracts that list to get all widget instances associated with our BakingWidgetProvider and refresh their state.

It Is widget(s) in the plural since the user can place as many widget instances as he sees fit on the home screen. So this code notifies all of them of the change so that they can update their views with the new information. 

Here is where the RemoteViews come into play: 
     


RemoteViews views = new RemoteViews(
             context.getPackageName(), R.layout.widget_grid_view);


Inflate the 'widget_grid_view' layout as the main container RemoteView and set its RemoteAdapter with:


Intent intent = new Intent(context, GridWidgetService.class);
views.setRemoteAdapter(R.id.widget_grid_view, intent);'
 

 

In essence GridWidgetService provides the RemoteView with its shape so it needs to extend the RemoteViewsService base class which connects the RemoteAdapter adapter to its RemoteView. Since it's a Service it also needs to be registered in our app's manifest:

 

 
 <service
            android:name=".widget.GridWidgetService"
            android:permission=
                      "android.permission.BIND_REMOTEVIEWS" />
           


GridWidgetService's nested inner class GridRemoteViewsFactory must implement the RemoteViewsService.RemoteViewsFactory interface which is a thin wrapper around the RemoteAdapter. In there we can find a typical Adapter's method counterparts, just named differently. As such, onBindViewHolder is replaced by getViewAt in the RemoteViewsFactory, getItemCount is replaced by getCount, while dataSetChanged is called once the RemoteViewsFactory is created and every time it gets notified to update its data through

appWidgetManager.notifyAppWidgetViewDataChanged(
               appWidgetIds, R.id.widget_grid_view)

   
image21

What's left is to set each Remote GridView cell (widget_grid_view_item) to the proper ingredient inside getViewAt().

 



Last Updated ( Monday, 20 November 2017 )