Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
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
Fragments In Action
Layouts
Fragments
Step 2 - Libraries & Networking
POJO To Parcelables
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':


/****
 baking_widget_info.xml
****/

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android=
                   "http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout=
                   "@layout/baking_widget_before_recipe"
    android:initialLayout="@layout/baking_widget_before_recipe"
    android:minHeight="80dp"
    android:minWidth="150dp"
    android:previewImage=
                  "@drawable/ic_chrome_reader_mode_black_48dp"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="1800000"
    android:widgetCategory="home_screen">  
</appwidget-provider>


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:

   
intent.getExtras().getStringArrayList(
                            FROM_ACTIVITY_INGREDIENTS_LIST);


 @Override
    public void onReceive(Context context, Intent intent) {
        AppWidgetManager appWidgetManager =
                                       AppWidgetManager.getInstance(context);
        int[] appWidgetIds = appWidgetManager
               .getAppWidgetIds(new ComponentName(
                                           context, BakingWidgetProvider.class));

        final String action = intent.getAction();

           if (action.equals("android.appwidget.action
                                               .APPWIDGET_UPDATE2")) {
               ingredientsList = intent.getExtras()
               .getStringArrayList(
                      FROM_ACTIVITY_INGREDIENTS_LIST);
               appWidgetManager
                     .notifyAppWidgetViewDataChanged(
                          appWidgetIds, R.id.widget_grid_view);
               //Now update all widgets
               BakingWidgetProvider
                  .updateBakingWidgets(
                      context, appWidgetManager, appWidgetIds);
               super.onReceive(context, intent);
           }
    }


The lines:

   
AppWidgetManager appWidgetManager =
                                  AppWidgetManager.getInstance(context);

int[] appWidgetIds = appWidgetManager
           .getAppWidgetIds(new ComponentName(
                            context, BakingWidgetProvider.class));


get all widget instances associated with our BakingWidgetProvider and refresh their state with:


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

BakingWidgetProvider.updateBakingWidgets(context,
                                     appWidgetManager, appWidgetIds)


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. 


BakingWidgetProvider.updateBakingWidgets(
                     context, appWidgetManager, appWidgetIds)


updateAppWidget does just that; it iterates over the widget collection and calls updateAppWidget on each:

 
public static void updateBakingWidgets(Context context,
                               AppWidgetManager appWidgetManager,
                                       int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }
   

 

updateAppWidget's body is:

 
static void updateAppWidget(Context context,
         AppWidgetManager appWidgetManager, int appWidgetId) {

        // Construct the RemoteViews object
        RemoteViews views =
            new RemoteViews(context.getPackageName(),
                                                          R.layout.widget_grid_view);

        //call activity when widget is clicked,
        //but resume activity from stack so you do not
       //pass intent.extras afresh
        Intent appIntent = new Intent(context, RecipeDetailActivity.class);
        appIntent.addCategory(Intent.ACTION_MAIN);
        appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
        appIntent.addFlags(
                    Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT|
                    Intent.FLAG_ACTIVITY_SINGLE_TOP);
        PendingIntent appPendingIntent = PendingIntent.getActivity(
                                              context, 0, appIntent, PendingIntent.
                                              FLAG_UPDATE_CURRENT);
        views.setPendingIntentTemplate(
                              R.id.widget_grid_view, appPendingIntent);

        // Set the GridWidgetService intent
        //to act as the adapter for the GridView
        Intent intent = new Intent(context, GridWidgetService.class);
        views.setRemoteAdapter(R.id.widget_grid_view, intent);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);

    }


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):


public class GridWidgetService extends RemoteViewsService {
    List<String> remoteViewingredientsList;

        @Override
        public RemoteViewsFactory onGetViewFactory(Intent intent) {
            return new GridRemoteViewsFactory(
                                     this.getApplicationContext(),intent);
        }

    class GridRemoteViewsFactory
             implements RemoteViewsService.RemoteViewsFactory {

        Context mContext = null;

        public GridRemoteViewsFactory(Context context,Intent intent) {
            mContext = context;

        }

        @Override
        public void onCreate() {
        }

        @Override
        public void onDataSetChanged() {
            remoteViewingredientsList = ingredientsList;
        }

        @Override
        public void onDestroy() {

        }

        @Override
        public int getCount() {
            return remoteViewingredientsList.size();
        }

        @Override
        public RemoteViews getViewAt(int position) {
            RemoteViews views =
               new RemoteViews(mContext.getPackageName(),
                            R.layout.widget_grid_view_item);

            views.setTextViewText(R.id.widget_grid_view_item,
                                       remoteViewingredientsList.get(position));

            Intent fillInIntent = new Intent();
            //fillInIntent.putExtras(extras);
            views.setOnClickFillInIntent(
                         R.id.widget_grid_view_item, fillInIntent);

            return views;
        }

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

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

        @Override
        public long getItemId(int position) {
            return position;
        }

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

      }
    }
   
   

   
image21

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


views.setTextViewText(R.id.widget_grid_view_item, 
                                        remoteViewingredientsList.get(position));


Back in BakingWidgetProvider there's a snippet of code that I haven't yet gone through:


       Intent appIntent = new Intent(context, RecipeDetailActivity.class);
       appIntent.addCategory(Intent.ACTION_MAIN);
       appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
        appIntent.addFlags(
                   Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT|
                   Intent.FLAG_ACTIVITY_SINGLE_TOP);
       
        PendingIntent appPendingIntent = PendingIntent.getActivity(
              context, 0, appIntent,
                 PendingIntent.FLAG_UPDATE_CURRENT);
        views.setPendingIntentTemplate(R.id.widget_grid_view,
                                                             appPendingIntent);


setPendingIntentTemplate tells what should happen when an item inside a cell of the remote GridView is clicked. In this case it should fire a PendingIntent which wraps an Intent (appIntent) that points to the RecipeDetailActivity, which means that upon clicking on a cell of an ingredient the RecipeDetailActivity should open at the particular recipe's ingredients list.

Usually, I would use setOnClickPendingIntent, but here setPendingIntentTemplate is used instead because clickicking needs to be activated on every individual Grid item.This also allows for customizing behavior at the cell level once GridRemoteViewsFactory getViewAt() fillInIntents is set:


Intent fillInIntent = new Intent();
//fillInIntent.putExtras(extras);
views.setOnClickFillInIntent(R.id.widget_grid_view_item, fillInIntent);
           


The other piece of code:


appIntent.addCategory(Intent.ACTION_MAIN);
appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
appIntent.addFlags(
                   Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT|
Intent.FLAG_ACTIVITY_SINGLE_TOP);


is just manipulating the Task stack to bring RecipeDetailActivity in front so that it is the first thing displayed on screen when a ingredient cell is clicked.



Last Updated ( Tuesday, 04 July 2017 )
 
 

   
Banner
RSS feed of all content
I Programmer - full contents
Copyright © 2017 i-programmer.info. All Rights Reserved.
Joomla! is Free Software released under the GNU/GPL License.