14. Weather Viewer App

Objectives

In this chapter you’ll:

• Use WeatherBug® web services to get the current conditions and five-day forecast for a specified city and process that data using an Android 3.x JsonReader.

• Use various types of Fragments to create reusable components and make better use of the screen real estate in a tablet app.

• Implement tabbed navigation using the Android 3.x ActionBar.

• Create a companion app widget that can be installed on the user’s home screen.

• Broadcast changes of the app’s preferred city to the companion app widget.

Outline

14.1 Introduction

14.2 Test-Driving the Weather Viewer App

14.3 Technologies Overview

14.4 Building the App’s GUI and Resource Files

14.4.1 AndroidManifest.xml

14.4.2 WeatherViewerActivity’s main.xml Layout

14.4.3 Default Cities and ZIP Codes in arrays.xml

14.4.4 WeatherViewerActivity’s actionmenu.xml Menu Layout

14.4.5 WeatherProvider App Widget Configuration and Layout

14.5 Building the App

14.5.1 Class WeatherViewerActivity

14.5.2 Class CitiesFragment

14.5.3 Class AddCityDialogFragment

14.5.4 Class ForecastFragment

14.5.5 Class SingleForecastFragment

14.5.6 Class ReadLocationTask

14.5.7 Class ReadForecastTask

14.5.8 Class FiveDayForecastFragment

14.5.9 Class ReadFiveDayForecastTask

14.5.10 Class DailyForecast

14.5.11 Class WeatherProvider

14.6 Wrap-Up

Self-Review Exercises | Answers to Self-Review Exercises | Exercises

14.1. Introduction

The Weather Viewer app (Fig. 14.1) uses WeatherBug® web services to obtain a city’s current weather conditions or its five-day weather forecast. The app is pre-populated with a list of cities in which Boston is set as the preferred city when you first install the app.

Image

Fig. 14.1. Weather Viewer app displaying the current weather conditions for Boston, MA.

This is an Android tablet app that takes advantage of various features which were introduced in Android 3.x. We use an Android 3.x JsonReader to read the weather data returned by the WeatherBug web services, which is returned to the app in JSON (JavaScript Object Notation) data format.

We use the Android 3.x action bar at the top of the screen, which is where menus and other app navigation elements are typically placed. You can add a new city by touching the Add New City option in the action bar. This displays a dialog (Fig. 14.2) in which you can enter a ZIP code and specify whether that city should be the preferred one. You can also switch between the current conditions and the five-day forecast (Fig. 14.3) by using the action bar’s tabbed navigation (Current Conditions and Five Day Forecast to the right of the app name in Fig. 14.1).

Image

Fig. 14.2. Add City dialog with a ZIP code entered and the Set as preferred city CheckBox checked.

Image

Fig. 14.3. Weather Viewer app displaying the five-day forecast for Sudbury, MA.

The list of cities, the current conditions, the five-day forecast and the dialogs in this app are implemented using Android 3.x fragments, which typically represent a reusable portion of an Activity’s user interface. An Activity can display multiple fragments to take advantage of tablet screen sizes. The list of cities is displayed as a ListFragment—a Fragment containing a ListView. Long pressing a city name in the list of cities displays a DialogFragment that allows you to remove that city or set it as the preferred one—the one for which the app displays the current conditions when it first loads. The dialog displayed when you touch Add New City in the action bar is also a DialogFragment. Touching a city’s name displays weather information for that city in a Fragment object.

This app also has a companion app widget (Fig. 14.4) that can be installed on one of your home screens. App widgets have been part of Android since its early versions. Android 3.x makes them resizable. The Weather Viewer app widget allows you to see your preferred city’s current weather conditions on the home screen of your choice.

Image

Fig. 14.4. Weather Viewer app’s companion app widget showing the current conditions for the preferred city that’s set in the app.

14.2. Test-Driving the Weather Viewer App

Opening and Running the App

Open Eclipse and import the Weather Viewer app project. To import the project:

1. Select File > Import... to display the Import dialog.

2. Expand the General node and select Existing Projects into Workspace, then click Next >.

3. To the right of the Select root directory: textfield, click Browse... then locate and select the WeatherViewer folder.

4. Click Finish to import the project.

The application receives weather data from the WeatherBug web services. To run this example, you must register for your own WeatherBug API key at

weather.weatherbug.com/desktop-weather/api.html

You (the developer) should note the WeatherBug API Terms of Use before making an app that you’d distribute in an app store. In the process of obtaining your API key at the site above, you’ll be asked to agree to the Terms of Use. Once you’ve obtained your API key, use it to replace YOUR_API_KEY on line 62 of class ReadLocationTask, line 66 of class ReadForecastTask and line 53 of class ReadFiveDayForecastTask. Next, right-click the app’s project in the Package Explorer window, then select Run As > Android Application from the menu that appears.

Viewing a City’s Current Weather Conditions and Five-Day Forecast

Touch a city in the list of cities to see its current weather conditions. Touch Five Day Forecast in the action bar at the top of the screen to switch to the five-day forecast view. Rotate your tablet between landscape and portrait modes to see the differences in the layouts for each orientation. You can return to the current weather conditions by touching Current Conditions in the action bar.

Adding a New City

Touch Add New City in the action bar to display the Add City dialog. Enter the ZIP code for the city you’d like to add. If you want this to be the preferred city, check the Set as preferred city CheckBox. Touch the Add City button to add the city to the list.

Removing a City from the City List and Changing the Preferred City

To remove a city from the city list or change the preferred city, long touch a city name to display a dialog with three buttons—Set as Preferred City, Delete and Cancel. Then touch the appropriate button for the task you wish to perform. If you delete the preferred city, the first city in the list is automatically set as the preferred one.

Adding the App Widget to Your Home Screen

To add this app’s associated home screen app widget, touch the home button on your device, then long touch in an empty spot on your home screen to display the list of widgets you can install. Scroll to the right until you find the Weather Viewer widget. Touch the widget to add it to the currently selected home screen, or drag the widget to one of the five home screens. Once you’ve added the widget, it automatically displays the current weather conditions for your preferred city. You can remove the widget by long touching it and dragging it over Remove in the upper-right corner of the screen. You can also resize the widget. To do so, long touch it then remove your finger from the screen. Android displays resizing handles that you can use to resize the widget.

14.3. Technologies Overview

Android 3.x Fragment, ListFragment and DialogFragment

Fragments are a key new feature of Android 3.x. A fragment typically represents a reusable portion of an Activity’s user interface, but it can also represent reusable logic code. This app focuses on using fragments to create and manage portions of the app’s GUI. You can combine several fragments to create robust user interfaces and to better take advantage of tablet screen sizes. You can also easily interchange fragments to make your GUIs more dynamic.

The base class of all fragments is Fragment (package android.app). This app uses several types of fragments. The list of cities is displayed as a ListFragment—a fragment containing a ListView. Dialog boxes are displayed using DialogFragments. The current weather conditions and the five-day forecast are displayed using subclasses of Fragment.

Though fragments were introduced in Android 3.x, there’s a compatibility package that enables you to use them with earlier versions of Android. You can get the latest version of this package at:

http://developer.android.com/sdk/compatibility-library.html

Managing Fragments

Like an Activity, each Fragment has a life cycle—we’ll discuss the Fragment life cycle methods as we encounter them. Fragments must be hosted in a parent Activity—they cannot be executed independently. The app’s main WeatherViewerActivity is the parent Activity for the app’s Fragments. The parent Activity uses a FragmentManager (package android.app) to manage the Fragments. A FragmentTransaction (package android.app) obtained from the FragmentManager allows the Activity to add, remove and transition between Fragments.

Fragment Layouts

Like an Activity, each Fragment has its own layout that’s typically defined as an XML layout resource, but also can be dynamically created. For the five-day forecast Fragment, we provide different layouts for landscape and portrait orientations, so we can better use the screen real estate available to the app. We display the five-day forecast from left to right in landscape orientation and from top to bottom in portrait orientation. We use the Activity’s Configuration (package android.content.res) to determine the current orientation, then specify the layout to use accordingly.

Android 3.x Action Bar

Android 3.x replaces the app’s title bar that was used in earlier Android versions with an action bar at the top of the screen. The app’s icon and name are displayed at the left side. In addition, the action bar can display the app’s options menu, navigation elements (such as tabbed navigation) and other interactive GUI components. In this app, we use the action bar to implement tabbed navigation between the current weather conditions Fragment and the five-day forecast Fragment for a particular city. The app also has an options menu with one option for adding a new city to the cities ListFragment. You can also designate menu items as actions that should be placed in the action bar if there’s room. To do so, you can set the menu item’s android:showAsAction attribute.

Handling Long Touches

When the user long touches an item in this app’s cities ListFragment, we’ll use an AdapterView.OnItemLongClickListener (package android.widget) to respond to that event and allow the user to set the selected city as the preferred one, delete the city or cancel the operation.

Companion App Widget

This app has a companion app widget that displays the current weather conditions for the user’s preferred city, as set in the Weather Viewer app. The user can long touch the home screen to select and add the widget. We extend class AppWidgetProvider (package android.appwidget), a subclass of BroadcastReceiver (package android.content), to create the app widget and allow it to receive notifications from the system when the app widget is enabled, disabled, deleted or updated.

PendingIntent to Launch an Activity from an App Widget

It’s common practice to allow a user to launch an app by touching the app’s companion widget on the device’s home screen. We use a PendingIntent (package android.app) to launch the app and display the current weather conditions for the preferred city.

Web Services and JsonReader

This app uses JsonReader (package android.util) to read JSON objects containing the weather data. We use a URL object to specify the URL that invokes the WeatherBug RESTful web service that returns JSON objects. We open an InputStream for that URL, which invokes the web service. The JsonReader gets its data from that InputStream.

Broadcast Intents and Receivers

The Weather Viewer’s companion app widget displays the current conditions for the preferred city, as currently set in the app. The user can change the preferred city at any time. When this occurs, the app uses an Intent to broadcast the change. The app widget uses a BroadcastReceiver (package android.content) to listen for this change so that it can display the current conditions for the appropriate city.

14.4. Building the App’s GUI and Resource Files

In this section, we review the new features in the GUI and resource files for the Weather Viewer app. To save space, we do not show this app’s strings.xml resource file, nor do we show most of the layout XML files.

14.4.1. AndroidManifest.xml

Figure 14.5 shows this app’s AndroidManifest.xml file. We set the uses-sdk element’s android:minSdkVersion attribute to "12" (line 5), which represents the Android 3.1 SDK. This app will run only on Android 3.1+ devices and AVDs. Lines 6–7 indicate that this app requires an Internet connection. The receiver element (lines 19–30) registers the WeatherProvider class (which represents the app widget) as a BroadcastReceiver, specifies the XML file for the app widget’s metadata and specifies WeatherProvider’s Intent filters. Line 32 registers WeatherProvider’s nested class WeatherService as a service, so that it can be launched to execute in the background. We use this WeatherService to update the weather data in our app widget. Like activities, all services must be registered in the manifest; otherwise, they cannot be executed.


  1   <?xml version="1.0" encoding="utf-8"?>
  2   <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3      package="com.deitel.weatherviewer" android:versionCode="1"
  4      android:versionName="1.0">
  5      <uses-sdk android:minSdkVersion="12" />
  6      <uses-permission android:name="android.permission.INTERNET">
  7      </uses-permission>
  8   
  9      <application android:icon="@drawable/icon"
 10         android:label="@string/app_name">
 11         <activity android:name=".WeatherViewerActivity"
 12            android:label="@string/app_name">
 13            <intent-filter>
 14               <action android:name="android.intent.action.MAIN" />
 15               <category android:name="android.intent.category.LAUNCHER" />
 16            </intent-filter>
 17         </activity>
 18         
 19         <receiver android:name=".WeatherProvider">                   
 20            <meta-data android:name="android.appwidget.provider"      
 21               android:resource="@xml/weather_widget_provider_info" />
 22            <intent-filter>                                           
 23               <action android:name=                                  
 24                  "android.appwidget.action.APPWIDGET_UPDATE" />      
 25            </intent-filter>                                          
 26            <intent-filter>                                           
 27               <action android:name=                                  
 28                  "com.deitel.weatherviewer.UPDATE_WIDGET" />         
 29            </intent-filter>                                          
 30         </receiver>                                                  
 31   
 32         <service android:name=".WeatherProvider$WeatherService" />
 33      </application>
 34   </manifest>


Fig. 14.5. AndroidManifest.xml.

14.4.2. WeatherViewerActivity’s main.xml Layout

The main.xml resource file (Fig. 14.6) defines the WeatherViewerActivity’s layout. We include a CitiesFragment as the first child of the root LinearLayout with the fragment element. The CitiesFragment will be created automatically when WeatherViewerActivity inflates its layout. We use the forecast_replacer FrameLayout as a placeholder in which we’ll display the ForecastFragments. By including this placeholder we define the size and location of the area in which the ForecastFragments will appear in the Activity. The WeatherViewerActivity swaps between ForecastFragments in this location using FragmentTransactions.


  1   <?xml version="1.0" encoding="utf-8"?>
  2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3      android:orientation="horizontal" android:layout_width="match_parent"
  4      android:layout_height="match_parent">
  5      <fragment class="com.deitel.weatherviewer.CitiesFragment"
  6         android:id="@+id/cities" android:layout_weight="3"    
  7         android:layout_width="wrap_content"                   
  8         android:layout_height="match_parent"/>                
  9      <FrameLayout android:layout_width="8dp"
 10         android:layout_height="match_parent"
 11         android:background="@android:color/black"/>
 12      <FrameLayout android:id="@+id/forecast_replacer"
 13         android:layout_width="match_parent"
 14         android:layout_height="match_parent"
 15         android:layout_weight="1" android:background="@android:color/white"/>
 16   </LinearLayout>


Fig. 14.6. WeatherViewerActivity’s main.xml layout.

14.4.3. Default Cities and ZIP Codes in arrays.xml

The default cities and their respective ZIP codes are stored in the app’s arrays.xml resource file (Fig. 14.7). This allows us to read lists of String resource values directly as opposed to reading each individually. The two String arrays are loaded in the WeatherViewerActivity by calling Resources method getStringArray.


  1   <?xml version="1.0" encoding="utf-8"?>
  2   <resources>
  3      <string-array name="default_city_names">
  4         <item>Boston</item>
  5         <item>Chicago</item>
  6         <item>Dallas</item>
  7         <item>Denver</item>
  8         <item>New York</item>
  9         <item>San Diego</item>
 10         <item>San Francisco</item>
 11         <item>Seattle</item>
 12      </string-array>
 13      <string-array name="default_city_zipcodes">
 14         <item>02115</item>
 15         <item>60611</item>
 16         <item>75254</item>
 17         <item>80202</item>
 18         <item>10024</item>
 19         <item>92104</item>
 20         <item>94112</item>
 21         <item>98101</item>
 22      </string-array>
 23   </resources>


Fig. 14.7. Default cities and ZIP codes in arrays.xml.

14.4.4. WeatherViewerActivity’s actionmenu.xml Menu Layout

The actionmenu.xml resource file (Fig. 14.8) defines the ActionBar’s menu items. The menu resource’s attributes are the same as those for the standard Android menu. We introduce the new attribute android:showAsAction which defines how a menu item should appear in the ActionBar. The value ifRoom specifies that this item should be visible in the ActionBar if there’s room to lay it out completely. You can force an item to appear in the ActionBar by using the always value but you risk overlapping menu items by doing so. The withText value specifies that the String value for the item’s android:title attribute is displayed with the menu item.


  1   <?xml version="1.0" encoding="utf-8"?>
  2   <menu xmlns:android="http://schemas.android.com/apk/res/android">
  3      <item android:id="@+id/add_city_item"
  4         android:icon="@android:drawable/ic_input_add"
  5         android:title="@string/add_new_city"
  6         android:showAsAction="ifRoom|withText"/>
  7   </menu>


Fig. 14.8. WeatherViewerActivity’s actionmenu.xml menu layout.

14.4.5. WeatherProvider App Widget Configuration and Layout

The weather_widget_provider_info.xml file (Fig. 14.9) defines the metadata for the WeatherViewer’s AppWidgetProvider. The minWidth and minHeight attributes describe the initial size of the app widget. So that home-screen icons and widgets can be sized and arranged uniformly, Android divides the home screen into equally sized cells, as described at:

http://developer.android.com/guide/practices/ui_guidelines/
   widget_design.html#sizes


  1   <?xml version="1.0" encoding="utf-8"?>
  2   <appwidget-provider
  3      xmlns:android="http://schemas.android.com/apk/res/android"
  4      android:minWidth="212dp" android:minHeight="148dp"
  5      android:initialLayout="@layout/weather_app_widget_layout"
  6      android:updatePeriodMillis="3600000"
  7      android:resizeMode="horizontal|vertical"/>


Fig. 14.9. WeatherProvider app widget configuration.

There are several standard widget sizes, one of which we’ve specified with the minWidth and minHeight attributes. The app widget’s layout resource is defined using the initialLayout attribute. The updatePeriodMillis attribute defines how often the AppWidgetProvider should receive the ACTION_APPWIDGET_UPDATE broadcast Intent. Each time this Intent is received, class WeatherProvider (Section 14.5.11) starts a new WeatherService to update the app widget’s current weather data. Any values for this attribute below 30 minutes are ignored. App widgets that require more frequent updates must do so using an AlarmManager. The android:resizeMode attribute is new to Android 3.1 and defines the directions in which the app widget can be resized on the home screen.

The app widget’s layout is specified in weather_app_widget_layout.xml, which uses a simple nested LinearLayout. We specified as the main LinearLayout’s background one of Google’s standard app widget borders, which you can download from

http://developer.android.com/guide/practices/ui_guidelines/
   widget_design.html#frames

14.5. Building the App

This app consists of 11 classes that are discussed in detail in Sections 14.5.114.5.11. Here we provide a brief overview of the classes and how they relate.

• Class WeatherViewerActivity (Section 14.5.1) is the app’s only Activity. The Activity uses an AddCityDialogFragment (Section 14.5.3) to allow the user to add new cities to the app. The Activity contains one instance of class CitiesFragment (Section 14.5.2) that’s always located at the left side of the screen. WeatherViewerActivity is responsible for swapping in and out the various ForecastFragments (Sections 14.5.4–, 14.5.5 and 14.5.8) that are displayed on the right side of the app. This Activity also contains the ActionBar code and loads the default cities and the cities that the user adds to the app.

• Class ReadLocationTask (Section 14.5.6) gets location information for a given ZIP code from the WeatherBug web services. It’s used in WeatherViewerActivity, both subclasses of ForecastFragment and the app widget.

• Class SingleForecastFragment (Section 14.5.5) is a Fragment that displays a single day’s forecast. The data that’s displayed is read by the AsyncTask ReadForecastTask (Section 14.5.7).

• Class FiveDayForecastFragment (Section 14.5.8) is similar to SingleForecastFragment, but it displays the five-day forecast, which is obtained by the AsyncTask ReadFiveDayForecastTask (Section 14.5.9). Class DailyForecast (Section 14.5.10) represents a single day’s forecast data. We use this class to simplify passing information back from the ReadFiveDayForecast task.

• Class WeatherProvider (Section 14.5.11) manages and updates the app widget. In addition to standard app widget broadcasts from the system, the widget receives broadcasts from the WeatherViewerActivity when the preferred city is changed.

14.5.1. Class WeatherViewerActivity

The WeatherViewerActivity class (Fig. 14.10) has several new import statements—the new features are highlighted. The class implements interface DialogFinishedListener (defined in Fig. 14.33) to so it can respond when the user adds a new city. We discuss the class’s fields as they’re used throughout this section.


  1   // WeatherViewerActivity.java
  2   // Main Activity for the Weather Viewer app.
  3   package com.deitel.weatherviewer;
  4   
  5   import java.util.HashMap;
  6   import java.util.Map;
  7   
  8   import android.app.ActionBar;            
  9   import android.app.ActionBar.Tab;        
 10   import android.app.ActionBar.TabListener;
 11   import android.app.Activity;
 12   import android.app.FragmentManager;    
 13   import android.app.FragmentTransaction;
 14   import android.content.Intent;
 15   import android.content.SharedPreferences;
 16   import android.content.SharedPreferences.Editor;
 17   import android.os.Bundle;
 18   import android.os.Handler;
 19   import android.view.Gravity;
 20   import android.view.Menu;
 21   import android.view.MenuInflater;
 22   import android.view.MenuItem;
 23   import android.widget.Toast;
 24   
 25   import com.deitel.weatherviewer.AddCityDialogFragment.
         DialogFinishedListener;
 26   import com.deitel.weatherviewer.CitiesFragment.CitiesListChangeListener;
 27   import com.deitel.weatherviewer.ReadLocationTask.LocationLoadedListener;
 28   
 29   public class WeatherViewerActivity extends Activity implements
 30      DialogFinishedListener
 31   {
 32      public static final String WIDGET_UPDATE_BROADCAST_ACTION =
 33         "com.deitel.weatherviewer.UPDATE_WIDGET";
 34   
 35      private static final int BROADCAST_DELAY = 10000;
 36   
 37      private static final int CURRENT_CONDITIONS_TAB = 0;
 38   
 39      public static final String PREFERRED_CITY_NAME_KEY =
 40         "preferred_city_name";
 41      public static final String PREFERRED_CITY_ZIPCODE_KEY =
 42         "preferred_city_zipcode";
 43      public static final String SHARED_PREFERENCES_NAME =
 44         "weather_viewer_shared_preferences";
 45      private static final String CURRENT_TAB_KEY = "current_tab";
 46      private static final String LAST_SELECTED_KEY = "last_selected";
 47   
 48      private int currentTab; // position of the current selected tab
 49      private String lastSelectedCity; // last city selected from the list
 50      private SharedPreferences weatherSharedPreferences;
 51   
 52      // stores city names and the corresponding zipcodes
 53      private Map<String, String> favoriteCitiesMap;
 54      private CitiesFragment listCitiesFragment;
 55      private Handler weatherHandler;
 56   


Fig. 14.10. Class WeatherViewerActivity package statement, import statements and fields.

WeatherViewerActivity method onCreate

Method onCreate (Fig. 14.11) initializes a new WeatherViewerActivity. We call Activity’s getFragmentManager method (line 66) to get the FragmentManager used to interact with this Activity’s Fragments—in this case, we get the CitiesFragment. The FragmentManager is also available to any of the Activity’s Fragments. In addition to initializing several other instance variables, we call setupTabs (defined in Fig. 14.26) to initialize the Activity’s ActionBar.


 57      // initializes this Activity and inflates its layout from xml
 58      @Override
 59      public void onCreate(Bundle savedInstanceState)
 60      {
 61         super.onCreate(savedInstanceState); // pass given Bundle to super
 62         setContentView(R.layout.main); // inflate layout in main.xml
 63   
 64         // get the CitiesFragment
 65         listCitiesFragment = (CitiesFragment)                 
 66            getFragmentManager().findFragmentById(R.id.cities);
 67      
 68         // set the CitiesListChangeListener
 69         listCitiesFragment.setCitiesListChangeListener(
 70            citiesListChangeListener);
 71   
 72         // create HashMap storing city names and corresponding ZIP codes
 73         favoriteCitiesMap = new HashMap<String, String>();
 74   
 75         weatherHandler = new Handler();
 76      
 77         weatherSharedPreferences = getSharedPreferences(
 78            SHARED_PREFERENCES_NAME, MODE_PRIVATE);
 79      
 80         setupTabs(); // set up the ActionBar's navigation tabs
 81      } // end method onCreate
 82   


Fig. 14.11. Overriding method onCreate in class WeatherViewerActivity.

WeatherViewerActivity methods onSaveInstanceState and onRestoreInstanceState

Method onSaveInstanceState (Fig. 14.12, lines 84–92) saves the current selected tab position and selected list item. The index of the currently selected tab is added to the given Bundle using Bundle’s putInt method. These values are read in the method onRestoreInstanceState (lines 95–104), allowing the Activity to display the same city and the same selected tab across orientation changes.


 83      // save this Activity's state
 84      @Override
 85      public void onSaveInstanceState(Bundle savedInstanceStateBundle)
 86      {
 87         // save the currently selected tab
 88         savedInstanceStateBundle.putInt(CURRENT_TAB_KEY, currentTab);
 89         savedInstanceStateBundle.putString(LAST_SELECTED_KEY,
 90            lastSelectedCity); // save the currently selected city
 91         super.onSaveInstanceState(savedInstanceStateBundle);
 92      } // end method onSaveInstanceState
 93   
 94      // restore the saved Activity state
 95      @Override
 96      public void onRestoreInstanceState(Bundle savedInstanceStateBundle)
 97      {
 98         super.onRestoreInstanceState(savedInstanceStateBundle);
 99   
 100        // get the selected tab
 101        currentTab = savedInstanceStateBundle.getInt(CURRENT_TAB_KEY);
 102        lastSelectedCity = savedInstanceStateBundle.getString(
 103           LAST_SELECTED_KEY); // get the selected city
 104     } // end method onRestoreInstanceState
 105  


Fig. 14.12. Overriding methods onSaveInstanceState and onRestoreInstanceState in class WeatherViewerActivity.

WeatherViewerActivity method onResume

We populate the favorite cities list in the Activity’s onResume method (Fig. 14.13). If the favoriteCitiesMap is empty, we read the saved cities from the app’s SharedPreferences by calling method loadSavedCities (Fig. 14.17). If there’s no data in the SharedPreferences the favoriteCitiesMap will still be empty. In this case, we call addSampleCities (Fig. 14.18) to add the pre-configured cities from XML resources. We specify the ActionBar’s currently selected tab using its selectTab method (line 124) then load the selected city’s forecast by calling loadSelectedForecast (Fig. 14.15).


 106     // called when this Activity resumes
 107     @Override
 108     public void onResume()
 109     {
 110        super.onResume();
 111  
 112        if (favoriteCitiesMap.isEmpty()) // if the city list is empty
 113        {
 114           loadSavedCities(); // load previously added cities
 115        } // end if
 116  
 117        // if there are no cities left
 118        if (favoriteCitiesMap.isEmpty())
 119        {
 120           addSampleCities(); // add sample cities
 121        } // end if
 122     
 123        // load previously selected forecast
 124        getActionBar().selectTab(getActionBar().getTabAt(currentTab));
 125        loadSelectedForecast();
 126     } // end method onResume
 127  


Fig. 14.13. Overriding WeatherViewerActivity method onResume.

Implementing CitiesListChangeListener

The CitiesListChangeListener (Fig. 14.14) receives updates from the CitiesFragment when the user selects a new city or changes the preferred one. Method onSelectedCityChanged (lines 133–138) is called when the user selects a new city. The given city name is passed to WeatherViewerActivity’s selectForecast method (Fig. 14.20) to display the selected city’s forecast in a ForecastFragment. Changes to the preferred city are reported to the onPreferredCityChanged method (lines 141–146). We pass the given city name to WeatherViewerActivity’s setPreferred method (Fig. 14.16) to update the app’s SharedPreferences.


 128     // listens for changes to the CitiesFragment
 129     private CitiesListChangeListener citiesListChangeListener =
 130        new CitiesListChangeListener()
 131     {
 132        // called when the selected city is changed
 133        @Override
 134        public void onSelectedCityChanged(String cityNameString)
 135        {
 136           // show the given city's forecast
 137           selectForecast(cityNameString);
 138        } // end method onSelectedCityChanged
 139  
 140        // called when the preferred city is changed
 141        @Override
 142        public void onPreferredCityChanged(String cityNameString)
 143        {
 144           // save the new preferred city to the app's SharedPreferences
 145           setPreferred(cityNameString);
 146        } // end method onPreferredCityChanged
 147     }; // end CitiesListChangeListener
 148  


Fig. 14.14. Implementing CitiesListChangeListener.

WeatherViewerActivity Method loadSelectedForecast

Method loadSelectedForecast (Fig. 14.15) calls method selectForecast (Fig. 14.20) to load the forecast of the last city that the user selected in the CitiesFragment. If no city is selected the preferred city’s forecast is loaded.


 149     // load the previously selected forecast
 150     private void loadSelectedForecast()
 151     {
 152        // if there was a previously selected city
 153        if (lastSelectedCity != null)
 154        {
 155           selectForecast(lastSelectedCity); // select last selected city
 156        } // end if
 157        else
 158        {
 159           // get the name of the preferred city
 160           String cityNameString = weatherSharedPreferences.getString(
 161              PREFERRED_CITY_NAME_KEY, getResources().getString(
 162              R.string.default_zipcode));
 163           selectForecast(cityNameString); // load preferred city's forecast
 164        } // end else
 165     } // end loadSelectedForecast
 166  


Fig. 14.15. WeatherViewerActivity method loadSelectedForecast.

WeatherViewerActivity Method setPreferred

Method setPreferred (Fig. 14.16) updates the preferred city entry in the app’s SharedPreferences. We get the ZIP code matching the given city name then get an Editor using SharedPreferences method edit. The name and ZIP code of the new preferred city are passed to Editor’s putString method. SharedPreferences method apply saves the changes. We clear the last selected city then call loadSelectedForecast (Fig. 14.15) to display the forecast of the new preferred city. Next, we create an Intent of type WIDGET_UPDATE_BROADCAST_ACTION and broadcast it using Activity’s sendBroadcast method. If the user installed the app widget on a home screen, the WeatherProvider (Section 14.5.11) will receive this broadcast and update the app widget to display the new preferred city’s forecast. Many web services, including those provided by WeatherBug, limit the number and frequency of calls you can make to the service. For this reason, we use a Handler to send the broadcast after a short delay—this prevents the app and the app widget from calling the web service at the same time to load the new forecast.


 167     // set the preferred city
 168     public void setPreferred(String cityNameString)
 169     {
 170        // get the give city's ZIP code
 171        String cityZipcodeString = favoriteCitiesMap.get(cityNameString);
 172        Editor preferredCityEditor = weatherSharedPreferences.edit();
 173        preferredCityEditor.putString(PREFERRED_CITY_NAME_KEY,
 174           cityNameString);
 175        preferredCityEditor.putString(PREFERRED_CITY_ZIPCODE_KEY,
 176           cityZipcodeString);
 177        preferredCityEditor.apply(); // commit the changes
 178        lastSelectedCity = null; // remove the last selected forecast
 179        loadSelectedForecast(); // load the preferred city's forecast
 180  
 181        // update the app widget to display the new preferred city
 182        final Intent updateWidgetIntent = new Intent(
 183           WIDGET_UPDATE_BROADCAST_ACTION);
 184  
 185        // send broadcast after short delay
 186        weatherHandler.postDelayed(new Runnable()
 187        {
 188           @Override
 189           public void run()
 190           {
 191              sendBroadcast(updateWidgetIntent); // broadcast the intent
 192           }
 193        }, BROADCAST_DELAY);
 194     } // end method setPreferred
 195  


Fig. 14.16. WeatherViewerActivity method setPreferred.

WeatherViewerActivity Method loadSavedCities

Method loadSavedCities (Fig. 14.17) loads the favorite cities list from the app’s SharedPreferences. A map of each city and ZIP code pair is obtained via SharedPreferences method getAll. We loop through the pairs and add them to the list using WeatherViewerActivity’s addCity method (Fig. 14.19).


 196     // reads previously saved city list from SharedPreferences
 197     private void loadSavedCities()
 198     {
 199        Map<String, ?> citiesMap = weatherSharedPreferences.getAll();
 200  
 201        for (String cityString : citiesMap.keySet())
 202        {
 203           // if this value is not the preferred city
 204           if (!(cityString.equals(PREFERRED_CITY_NAME_KEY) ||
 205              cityString.equals(PREFERRED_CITY_ZIPCODE_KEY)))
 206           {
 207              addCity(cityString, (String) citiesMap.get(cityString), false);
 208           } // end if
 209        } // end for
 210     } // end method loadSavedCities
 211  


Fig. 14.17. WeatherViewerActivity method loadSavedCities.

WeatherViewerActivity Method addSampleCities

Method addSampleCities (Fig. 14.18) method reads the default favorite cities from the app’s arrays.xml resource file. We use class Resource’s getStringArray method (lines 216–217 and 220–221) to retrieve arrays containing the default city names and ZIP codes. We loop through each city and add it to the list using the addCity method (Fig. 14.19). The first sample city’s name is passed to WeatherViewerActivity’s setPreferred method to select it as the preferred city (Fig. 14.16).


 212     // add the sample cities
 213     private void addSampleCities()
 214     {
 215        // load the array of city names from resources
 216        String[] sampleCityNamesArray = getResources().getStringArray(
 217           R.array.default_city_names);                               
 218     
 219        // load the array of ZIP codes from resources
 220        String[] sampleCityZipcodesArray = getResources().getStringArray(
 221           R.array.default_city_zipcodes);
 222  
 223        // for each sample city
 224        for (int i = 0; i < sampleCityNamesArray.length; i++)
 225        {
 226           // set the first sample city as the preferred city by default
 227           if (i == 0)
 228           {
 229              setPreferred(sampleCityNamesArray[i]);
 230           } // end if
 231           
 232           // add city to the list
 233           addCity(sampleCityNamesArray[i], sampleCityZipcodesArray[i],
 234              false);
 235        } // end for
 236     } // end method addSampleCities
 237  


Fig. 14.18. WeatherViewerActivity method addSampleCities.

WeatherViewerActivity Method addCity

New cities are added to the CitiesFragment (Section 14.5.2) using the addCity method (Fig. 14.19). The given city name and ZIP code are added to the favoriteCitiesMap then passed to CitiesFragment’s addCity method. We also add the city to the app’s SharedPreferences and call apply to save the new city.


 238     // add a new city to the CitiesFragment ListFragment
 239     public void addCity(String city, String zipcode, boolean select)
 240     {
 241        favoriteCitiesMap.put(city, zipcode); // add to HashMap of cities
 242        listCitiesFragment.addCity(city, select); // add city to Fragment
 243        Editor preferenceEditor = weatherSharedPreferences.edit();
 244        preferenceEditor.putString(city, zipcode);
 245        preferenceEditor.apply();
 246     } // end method addCity
 247  


Fig. 14.19. WeatherViewerActivity method addCity.

WeatherViewerActivity Method selectForecast

Method selectForecast (Fig. 14.20) displays the forecast information for the given city. We get the current visible forecast Fragment using FragmentManager’s findFragmentById method. We pass to this method the ID of the FrameLayout in the Activity’s layout. The first time this method executes, the result will be null. The FragmentManager can access the visible forecast Fragment after we replace the FrameLayout with a Fragment during a FragmentTransaction. If the current selected ActionBar tab is the Current Conditions tab, we create a new ForecastFragment using the given ZIP code (lines 270–271). Otherwise, the Five Day Forecast Tab must be selected, so we create a new FiveDayForecastFragment (lines 276–277). We create a new FragmentTransaction using FragmentManager’s beginTransaction method (lines 281–282). FragmentTransactions are used to add, remove and replace Fragments, among other interactions. In this case, we’ll replace the Fragment on the right half of the Activity with the new Fragment we just created. We pass FragmentTransaction’s TRANSIT_FRAGMENT_FADE constant to its setTransition method (285–286) to visually fade the old Fragment into the new one. Next we call ForecastFragment’s replace method (lines 290–291) with the ID of the item to be replaced and the Fragment to take its place. FragmentTransaction’s commit method (line 293) executes the transaction.


 248     // display forecast information for the given city
 249     public void selectForecast(String name)
 250     {
 251        lastSelectedCity = name; // save the city name
 252        String zipcodeString = favoriteCitiesMap.get(name);
 253        if (zipcodeString == null) // if the ZIP code can't be found
 254        {
 255           return; // do not attempt to load a forecast
 256        } // end if
 257  
 258        // get the current visible ForecastFragment
 259        ForecastFragment currentForecastFragment = (ForecastFragment)    
 260           getFragmentManager().findFragmentById(R.id.forecast_replacer);
 261  
 262        if (currentForecastFragment == null ||
 263           !(currentForecastFragment.getZipcode().equals(zipcodeString) &&
 264           correctTab(currentForecastFragment)))
 265        {
 266           // if the selected current tab is "Current Conditions"
 267           if (currentTab == CURRENT_CONDITIONS_TAB)
 268           {
 269              // create a new ForecastFragment using the given ZIP code
 270              currentForecastFragment = SingleForecastFragment.newInstance(
 271                 zipcodeString);
 272           } // end if
 273           else
 274           {
 275              // create a new ForecastFragment using the given ZIP code
 276              currentForecastFragment = FiveDayForecastFragment.newInstance(
 277                 zipcodeString);
 278           } // end else
 279  
 280           // create a new FragmentTransaction
 281           FragmentTransaction forecastFragmentTransaction =
 282              getFragmentManager().beginTransaction();      
 283  
 284           // set transition animation to fade
 285           forecastFragmentTransaction.setTransition(    
 286              FragmentTransaction.TRANSIT_FRAGMENT_FADE);
 287  
 288           // replace the Fragment (or View) at the given id with our
 289           // new Fragment
 290           forecastFragmentTransaction.replace(R.id.forecast_replacer,
 291              currentForecastFragment);
 292     
 293           forecastFragmentTransaction.commit(); // begin the transition
 294        } // end if
 295     } // end method selectForecast
 296  


Fig. 14.20. WeatherViewerActivity method selectForecast.

WeatherViewerActivity Methods correctTab and selectTab

Method correctTab (Fig. 14.21, lines 298–313) returns true if the given ForecastFragment matches the currently selected tab—in particular, when the Current Conditions tab is selected and it’s given a SingleForecastFragment or when the Five Day Forecast tab is selected and it’s given a FiveDayForecastFragment. The selectForecast method uses this information to determine whether it needs to update the visible ForecastFragment. Method selectTab (lines 316–320) selects the tab at the given index. We save the index to the currentTab instance variable then call loadSelectedForecast (Fig. 14.15).


 297     // is this the proper ForecastFragment for the currently selected tab?
 298     private boolean correctTab(ForecastFragment forecastFragment)
 299     {
 300        // if the "Current Conditions" tab is selected
 301        if (currentTab == CURRENT_CONDITIONS_TAB)
 302        {
 303           // return true if the given ForecastFragment
 304           // is a SingleForecastFragment
 305           return (forecastFragment instanceof SingleForecastFragment);
 306        } // end if
 307        else // the "Five Day Forecast" tab is selected
 308        {
 309           // return true if the given ForecastFragment
 310           // is a FiveDayForecastFragment
 311           return (forecastFragment instanceof FiveDayForecastFragment);
 312        } // end else
 313     } // end method correctTab
 314  
 315     // select the tab at the given position
 316     private void selectTab(int position)
 317     {
 318        currentTab = position; // save the position tab
 319        loadSelectedForecast();
 320     } // end method selectTab
 321  


Fig. 14.21. WeatherViewerActivity methods correctTab and selectTab.

Overriding Activity Methods onCreateOptionsMenu and onOptionsItemSelected

Method onCreateOptionsMenu (Fig. 14.22, lines 323–332) initializes the Add New City button in the ActionBar. We get the global MenuInflator using Activity’s getMenuInflator method. We inflate the menu defined in actionmenu.xml and attach it to the given Menu object. Method onOptionsItemSelected (lines 335–346) is called when the user touches the Add New City item on the ActionBar. We confirm that the MenuItem matches the expected resource ID then call showAddCityDialog (Fig. 14.23) to display an AddCityDialogFragment (Section 14.5.3). We return true to indicate that the menu item selection was handled in this method.


 322     // create this Activities Menu
 323     @Override
 324     public boolean onCreateOptionsMenu(Menu menu)
 325     {
 326        super.onCreateOptionsMenu(menu);
 327        MenuInflater inflater = getMenuInflater(); // global MenuInflator
 328  
 329        // inflate layout defined in actionmenu.xml
 330        inflater.inflate(R.menu.actionmenu, menu);
 331        return true; // return true since the menu was created
 332     } // end method onCreateOptionsMenu
 333  
 334     // when one of the items was clicked
 335     @Override
 336     public boolean onOptionsItemSelected(MenuItem item)
 337     {
 338        // if the item selected was the "Add City" item
 339         if (item.getItemId() == R.id.add_city_item)
 340         {
 341            showAddCityDialog(); // show Dialog for user input
 342            return true; // return true since we handled the selection
 343         } // end if
 344  
 345         return false; // do not handle unexpected menu items
 346      } // end method onOptionsItemSelected
 347  


Fig. 14.22. Overriding Activity methods onCreateOptionsMenu and onOptionsItemSelected.

WeatherViewerActivity Methods showAddCityDialog and onDialogFinished

Method showAddCityDialog (Fig. 14.23, lines 349–364) displays a DialogFragment allowing the user to enter a ZIP code. After creating a new AddCityDialogFragment, weget the Activity’s FragmentManager (line 356). We create a new FragmentTransaction using FragmentManager’s beginTransaction method. We pass the FragmentTransaction to DialogFragment’s show method to display it over the Activity. Although not demonstrated here, it’s also possible to embed a FragmentDialog in the Activity’s View hierarchy. Method onDialogFinished (lines 367–372) is called when the AddCityDialog is dismissed. The zipcodeString argument represents the user-entered ZIP code. The boolean argument preferred is true if the user checks the Set as preferred city CheckBox. We pass both of these to method getCityNameFromZipcode (Fig. 14.24).


 348     // display FragmentDialog allowing the user to add a new city
 349     private void showAddCityDialog()
 350     {
 351        // create a new AddCityDialogFragment
 352        AddCityDialogFragment newAddCityDialogFragment =
 353           new AddCityDialogFragment();
 354        
 355        // get instance of the FragmentManager
 356        FragmentManager thisFragmentManager = getFragmentManager();
 357        
 358        // begin a FragmentTransaction
 359        FragmentTransaction addCityFragmentTransition =
 360           thisFragmentManager.beginTransaction();        
 361  
 362        // show the DialogFragment
 363        newAddCityDialogFragment.show(addCityFragmentTransition, "");
 364     } // end method showAddCityDialog
 365  
 366     // called when the FragmentDialog is dismissed
 367     @Override
 368     public void onDialogFinished(String zipcodeString, boolean preferred)
 369     {
 370        // convert ZIP code to city
 371        getCityNameFromZipcode(zipcodeString, preferred);
 372     } // end method onDialogFinished
 373  


Fig. 14.23. WeatherViewerActivity methods showAddCityDialog and onDialogFinished.

WeatherViewerActivity Methods getCityNameFromZipcode

Method getCityNameFromZipcode (Fig. 14.24) launches a new ReadLocationTask (Section 14.5.6) to retrieve the city name for the given ZIP code. If the ZIP code is already in the favorite cities list, we do not launch the AsyncTask but instead display a Toast indicating that the user cannot add duplicate cities.


 374     // read city name from ZIP code
 375     private void getCityNameFromZipcode(String zipcodeString,
 376        boolean preferred)
 377     {
 378        // if this ZIP code is already added
 379        if (favoriteCitiesMap.containsValue(zipcodeString))
 380        {
 381           // create a Toast displaying error information
 382           Toast errorToast = Toast.makeText(WeatherViewerActivity.this,
 383              WeatherViewerActivity.this.getResources().getString(
 384              R.string.duplicate_zipcode_error), Toast.LENGTH_LONG);
 385              errorToast.setGravity(Gravity.CENTER, 0, 0);
 386              errorToast.show(); // show the Toast
 387        } // end if
 388        else
 389        {
 390           // load the location information in a background thread
 391           new ReadLocationTask(zipcodeString, this,
 392              new CityNameLocationLoadedListener(zipcodeString, preferred)).
 393              execute();
 394        } // end else
 395     } // end method getCityNameFromZipcode
 396  


Fig. 14.24. WeatherViewerActivity methods getCityNameFromZipcode.

Implementing Interface LocationLoadedListener

The CityNameLocationLoadedListener (Fig. 14.25) receives information from a completed ReadLocationTask. When the LocationLoadedListener is constructed we specify whether or not this location is the preferred city using the boolean parameter preferred. We add the city to the favorite city list by passing the city name and ZIP code to WeatherViewerActivity’s addCity method. The third argument to this method determines whether or not the new city’s forecast is loaded. If the new city is set to be the preferred city we pass the city name to setPreferred.


 397     // listens for city information loaded in background task
 398     private class CityNameLocationLoadedListener implements
 399        LocationLoadedListener
 400     {
 401        private String zipcodeString; // ZIP code to look up
 402        private boolean preferred;
 403  
 404        // create a new CityNameLocationLoadedListener
 405        public CityNameLocationLoadedListener(String zipcodeString,
 406           boolean preferred)
 407        {
 408           this.zipcodeString = zipcodeString;
 409           this.preferred = preferred;
 410        } // end CityNameLocationLoadedListener
 411        
 412        @Override
 413        public void onLocationLoaded(String cityString, String stateString,
 414           String countryString)
 415        {
 416            // if a city was found to match the given ZIP code
 417           if (cityString != null)
 418           {
 419              addCity(cityString, zipcodeString, !preferred); // add new city
 420        
 421              if (preferred) // if this location is the preferred city
 422              {
 423                 // save the preferred city to SharedPreferences
 424                 setPreferred(cityString);
 425              } // end if
 426           } // end if
 427           else
 428           {
 429              // display a text explaining that location information could
 430              // not be found
 431              Toast zipcodeToast = Toast.makeText(WeatherViewerActivity.this,
 432                 WeatherViewerActivity.this.getResources().getString(
 433                 R.string.invalid_zipcode_error), Toast.LENGTH_LONG);
 434              zipcodeToast.setGravity(Gravity.CENTER, 0, 0);
 435              zipcodeToast.show(); // show the Toast
 436           } // end else
 437        } // end method onLocationLoaded
 438     } // end class CityNameLocationLoadedListener
 439  


Fig. 14.25. Implementing interface LocationLoadedListener.

WeatherViewerActivity Method setupTabs

The ActionBar’s tabbed navigation is initialized in the setupTabs method (Fig. 14.26). We call Activity’s getActionBar method to get a reference to its ActionBar. The ActionBar replaces the title bar in all 3.x apps and provides capabilities that allow users to navigate the app with tabs and drop-down menus. Next, we pass ActionBar’s NAVIGATION_MODE_TABS constant to its setNavigationMode method to indicate we’ll be using Tabs. We create two Tab objects with ActionBar’s newTab method (lines 449 and 460) to allow the user to select between the current weather conditons and the five-day forecast. For each Tab, we set its text and register its TabListener (weatherTabListener, defined in Fig. 14.27). Lines 457 and 464 add the Tabs to the ActionBar with ActionBar’s addTab method. We create two Tabs, one for the Current Conditions and one for the Five Day Forecast.


 440     // set up the ActionBar's tabs
 441     private void setupTabs()
 442     {
 443        ActionBar weatherActionBar = getActionBar(); // get the ActionBar
 444  
 445        // set ActionBar's navigation mode to use tabs
 446        weatherActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
 447  
 448        // create the "Current Conditions" Tab
 449        Tab currentConditionsTab = weatherActionBar.newTab();
 450  
 451        // set the Tab's title
 452        currentConditionsTab.setText(getResources().getString(
 453           R.string.current_conditions));
 454  
 455        // set the Tab's listener
 456        currentConditionsTab.setTabListener(weatherTabListener);     
 457        weatherActionBar.addTab(currentConditionsTab); // add the Tab
 458  
 459        // create the "Five Day Forecast" tab
 460        Tab fiveDayForecastTab = weatherActionBar.newTab();   
 461        fiveDayForecastTab.setText(getResources().getString(  
 462           R.string.five_day_forecast));                      
 463        fiveDayForecastTab.setTabListener(weatherTabListener);
 464        weatherActionBar.addTab(fiveDayForecastTab);          
 465  
 466        // select "Current Conditions" Tab by default
 467        currentTab = CURRENT_CONDITIONS_TAB;
 468     } // end method setupTabs
 469  


Fig. 14.26. WeatherViewerActivity method setupTabs.

Implementing Interface TabListener

Figure 14.27 implements TabListener to handle the events that occur when the user selects the tabs created in Fig. 14.26. Method onTabSelected (lines 480–485) calls function selectTab (Fig. 14.21) with the selected Tab’s index to display the appropriate weather data.


 470     // listen for events generated by the ActionBar Tabs
 471     TabListener weatherTabListener = new TabListener()
 472     {
 473        // called when the selected Tab is re-selected
 474        @Override
 475        public void onTabReselected(Tab arg0, FragmentTransaction arg1)
 476        {
 477        } // end method onTabReselected
 478     
 479        // called when a previously unselected Tab is selected
 480        @Override
 481        public void onTabSelected(Tab tab, FragmentTransaction arg1)
 482        {
 483           // display the information corresponding to the selected Tab
 484           selectTab(tab.getPosition());
 485        } // end method onTabSelected
 486  
 487        // called when a tab is unselected
 488        @Override
 489        public void onTabUnselected(Tab arg0, FragmentTransaction arg1)
 490        {
 491        } // end method onTabSelected
 492     }; // end WeatherTabListener
 493  } // end Class WeatherViewerActivity


Fig. 14.27. Implementing interface TabListener.

14.5.2. Class CitiesFragment

The CitiesFragment defines a ListFragment designed to hold a list of cities. The WeatherViewerActivity’s View hierarchy includes one CitiesFragment which remains pinned to the left side of the Activity at all times.

CitiesFragment package Statement, import Statements, Fields and CitiesListChangeListener Nested Interface

Fig. 14.28 begins the definition of class CitiesFragment. This Fragment reports user interactions to its parent Activity, which implements the nested interface CitiesListChangeListener (lines 40–47; implemented in Fig. 14.14). Method onSelectedCityChanged is called when the user touches a city name in the list of cities. Method onPreferredCityChanged reports changes to the preferred city.


  1   // CitiesFragment.java
  2   // Fragment displaying list of favorite cities.
  3   package com.deitel.weatherviewer;
  4   
  5   import java.util.ArrayList;
  6   import java.util.List;
  7   
  8   import android.app.AlertDialog;
  9   import android.app.ListFragment;
 10   import android.content.Context;
 11   import android.content.DialogInterface;
 12   import android.content.SharedPreferences;
 13   import android.content.SharedPreferences.Editor;
 14   import android.content.res.Resources;
 15   import android.graphics.Color;
 16   import android.os.Bundle;
 17   import android.view.Gravity;
 18   import android.view.View;
 19   import android.view.ViewGroup;
 20   import android.widget.AdapterView;
 21   import android.widget.AdapterView.OnItemLongClickListener;
 22   import android.widget.ArrayAdapter;
 23   import android.widget.ListView;
 24   import android.widget.TextView;
 25   import android.widget.Toast;
 26   
 27   public class CitiesFragment extends ListFragment
 28   {
 29      private int currentCityIndex; // the currently selected list position
 30      
 31      // key used to save list selection in a Bundle
 32      private static final String CURRENT_CITY_KEY = "current_city";
 33   
 34      public ArrayList<String> citiesArrayList; // list of city names
 35      private CitiesListChangeListener citiesListChangeListener;
 36      private ArrayAdapter<String> citiesArrayAdapter;
 37   
 38      // interface describing listener for changes to selected city and
 39      // preferred city
 40      public interface CitiesListChangeListener
 41      {
 42         // the selected city is changed
 43         public void onSelectedCityChanged(String cityNameString);
 44         
 45         // the preferred city is changed
 46         public void onPreferredCityChanged(String cityNameString);
 47      } // end interface CitiesListChangeListener
 48   


Fig. 14.28. CitiesFragment package statement, import Statements, fields and CitiesListChangeListener nested interface.

CitiesFragment Methods onActivityCreated and setCitiesListChangeListener

Method onActivityCreated (Fig. 14.29, lines 50–78) initializes this ListFragment’s ListView. We first check if the given Bundle is null. If not, the selected city is retrieved using Bundle’s getInt method. This allows us to persist the selected list item across orientation changes. We then create a new ListAdapter of type CitiesArrayAdapter (Fig. 14.30) using the Activity’s context, the list item layout in city_list_item.xml and an empty ArrayList. We also indicate that the ListView should allow only one choice at a time, and register its OnLongItemClickListener, so the user can set the city as the preferred one or delete it.


 49      // called when the parent Activity is created
 50      @Override
 51      public void onActivityCreated(Bundle savedInstanceStateBundle)
 52      {
 53         super.onActivityCreated(savedInstanceStateBundle);
 54      
 55         // the the given Bundle has state information
 56         if (savedInstanceStateBundle != null)
 57         {
 58            // get the last selected city from the Bundle
 59            currentCityIndex = savedInstanceStateBundle.getInt(
 60               CURRENT_CITY_KEY);
 61         } // end if
 62   
 63         // create ArrayList to save city names
 64         citiesArrayList = new ArrayList<String>();
 65   
 66         // set the Fragment's ListView adapter
 67         setListAdapter(new CitiesArrayAdapter<String>(getActivity(),
 68            R.layout.city_list_item, citiesArrayList));
 69      
 70         ListView thisListView = getListView(); // get the Fragment's ListView
 71         citiesArrayAdapter = (ArrayAdapter<String>)getListAdapter();
 72   
 73         // allow only one city to be selected at a time
 74         thisListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
 75         thisListView.setBackgroundColor(Color.WHITE); // set background color
 76         thisListView.setOnItemLongClickListener(
 77            citiesOnItemLongClickListener);
 78      } // end method onActivityCreated
 79   
 80      // set CitiesListChangeListener
 81      public void setCitiesListChangeListener(
 82         CitiesListChangeListener listener)
 83      {
 84         citiesListChangeListener = listener;
 85      } // end method setCitiesChangeListener
 86   


Fig. 14.29. CitiesFragment methods onActivityCreated and setCitiesListChangeListener.

Method setCitiesListChangeListener (lines 81–85) allows the parent Activity to set this CitiesFragment’s CitiesListChangeListener. This listener reports changes in the CitiesFragment to the WeatherViewerActivity.

CitiesFragment Nested Class CitiesArrayAdapter

The CitiesArrayAdapter (Fig. 14.30) is a custom ArrayAdapter which displays each city name in a list item. A star icon is placed to the left of the preferred city’s name. The getView method (line 101–122) is called each time the Fragment’s ListView needs a new list item View. We first save the results from the call to the superclass’s getView method, ensuring that an existing View is reused if one is available. We pass the city name for this list item to the isPreferredCity method (125–136). If this is the preferred city we display the star icon using TextView’s setCompoundDrawables method. If not, we use the same method to clear any previous star. Method isPreferredCity returns true if the given String matches the preferred city’s name. We use the parent Activity’s Context to access the app’s shared preferences then compare the given String to the preferred city name.


 87      // custom ArrayAdapter for CitiesFragment ListView
 88      private class CitiesArrayAdapter<T> extends ArrayAdapter<String>
 89      {
 90         private Context context; // this Fragment's Activity's Context
 91         
 92         // public constructor for CitiesArrayAdapter
 93         public CitiesArrayAdapter(Context context, int textViewResourceId,
 94            List<String> objects)
 95         {
 96            super(context, textViewResourceId, objects);
 97            this.context = context;
 98         } // end CitiesArrayAdapter constructor
 99   
 100        // get ListView item for the given position
 101        @Override
 102        public View getView(int position, View convertView, ViewGroup parent)
 103        {
 104           // get the TextView generated by ArrayAdapter's getView method
 105           TextView listItemTextView = (TextView)
 106              super.getView(position, convertView, parent);
 107  
 108           // if this item is the preferred city
 109           if (isPreferredCity(listItemTextView.getText().toString()))
 110           {
 111              // display a star to the right of the list item TextView
 112              listItemTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0,
 113                 android.R.drawable.btn_star_big_on, 0);                    
 114           } // end if
 115           else
 116           {
 117              // clear any compound drawables on the list item TextView
 118              listItemTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0,
 119                 0, 0);                                                     
 120           } // end else
 121           return listItemTextView;
 122        } // end method getView
 123  
 124        // is the given city the preferred city?
 125        private boolean isPreferredCity(String cityString)
 126        {
 127           // get the app's SharedPreferences
 128           SharedPreferences preferredCitySharedPreferences =
 129              context.getSharedPreferences(
 130              WeatherViewerActivity.SHARED_PREFERENCES_NAME,
 131              Context.MODE_PRIVATE);
 132     
 133           // return true if the given name matches preferred city's name
 134           return cityString.equals(preferredCitySharedPreferences.getString(
 135              WeatherViewerActivity.PREFERRED_CITY_NAME_KEY, null));
 136        } // end method isPreferredCity
 137     } // end class CitiesArrayAdapter
 138  


Fig. 14.30. CitiesFragment nested class CitiesArrayAdapter.

Implementing Interface OnItemLongClickListener

The citiesOnItemLongClickListener (Fig. 14.31) responds to long presses on the Fragment’s ListView items. We construct an AlertDialog allowing the user to delete the selected item or set it as the preferred city. We use AlertDialog.Builder’s setPositiveButton method to construct the Set Preferred option. The OnClickListener’s onClick method for this Button (lines 172–177) passes the selected city’s name to the CitiesListChangeListener’s onPreferredCityChanged method. ArrayAdapter’s notifyDataSetChanged method refreshes the ListView. We then create a Button for the Delete option, which removes the selected city from the app. In its onClick method, lines 185–233), we first check if the selected item is the only item in the list using ArrayAdapter’s getCount method, in which case we do not allow it to be deleted and display a Toast. Otherwise the item is deleted using ArrayAdapter’s remove method. We then delete the city name from the app’s shared preferences. If the deleted city was previously the preferred city, we select the first city in the list as the new preferred city. Otherwise, we ask the WeatherViewerActivity to display the preferred city’s forecast by passing its name to the CitiesListChangeListener’s onSelectedCityChanged method.


 139     // responds to events generated by long pressing ListView item
 140     private OnItemLongClickListener citiesOnItemLongClickListener =
 141        new OnItemLongClickListener()
 142     {
 143        // called when a ListView item is long-pressed
 144        @Override
 145        public boolean onItemLongClick(AdapterView<?> listView, View view,
 146           int arg2, long arg3)
 147        {
 148           // get the given View's Context
 149           final Context context = view.getContext();
 150     
 151           // get Resources to load Strings from xml
 152           final Resources resources = context.getResources();
 153  
 154           // get the selected city's name
 155           final String cityNameString =
 156              ((TextView) view).getText().toString();
 157  
 158           // create a new AlertDialog
 159           AlertDialog.Builder builder = new AlertDialog.Builder(context);
 160  
 161           // set the AlertDialog's message
 162           builder.setMessage(resources.getString(
 163              R.string.city_dialog_message_prefix) + cityNameString +
 164              resources.getString(R.string.city_dialog_message_postfix));
 165  
 166           // set the AlertDialog's positive Button
 167           builder.setPositiveButton(resources.getString(
 168              R.string.city_dialog_preferred),
 169              new DialogInterface.OnClickListener()
 170              {
 171                 @Override
 172                 public void onClick(DialogInterface dialog, int which)
 173                 {
 174                    citiesListChangeListener.onPreferredCityChanged(
 175                       cityNameString);
 176                    citiesArrayAdapter.notifyDataSetChanged();
 177                 } // end method onClick
 178            }); // end DialogInterface.OnClickListener
 179           // set the AlertDialog's neutral Button
 180           builder.setNeutralButton(resources.getString(
 181              R.string.city_dialog_delete),
 182              new DialogInterface.OnClickListener()
 183              {
 184                 // called when the "Delete" Button is clicked
 185                 public void onClick(DialogInterface dialog, int id)
 186                 {
 187                    // if this is the last city
 188                    if (citiesArrayAdapter.getCount() == 1)
 189                    {
 190                       // inform the user they can't delete the last city
 191                       Toast lastCityToast =
 192                          Toast.makeText(context, resources.getString(
 193                          R.string.last_city_warning), Toast.LENGTH_LONG);
 194                       lastCityToast.setGravity(Gravity.CENTER, 0, 0);
 195                       lastCityToast.show(); // show the Toast
 196                       return; // exit the method
 197                    } // end if
 198  
 199                    // remove the city
 200                    citiesArrayAdapter.remove(cityNameString);
 201  
 202                    // get the app's shared preferences
 203                    SharedPreferences sharedPreferences =
 204                       context.getSharedPreferences(
 205                       WeatherViewerActivity.SHARED_PREFERENCES_NAME,
 206                       Context.MODE_PRIVATE);
 207     
 208                    // remove the deleted city from SharedPreferences
 209                    Editor preferencesEditor = sharedPreferences.edit();
 210                    preferencesEditor.remove(cityNameString);
 211                    preferencesEditor.apply();
 212  
 213                    // get the current preferred city
 214                    String preferredCityString =
 215                       sharedPreferences.getString(
 216                          WeatherViewerActivity.PREFERRED_CITY_NAME_KEY,
 217                          resources.getString(R.string.default_zipcode));
 218  
 219                    // if the preferred city was deleted
 220                    if (cityNameString.equals(preferredCityString))
 221                    {
 222                       // set a new preferred city
 223                       citiesListChangeListener.onPreferredCityChanged(
 224                          citiesArrayList.get(0));
 225                    } // end if
 226                    else if (cityNameString.equals(citiesArrayList.get(
 227                       currentCityIndex)))
 228                    {
 229                      // load the preferred city's forecast
 230                      citiesListChangeListener.onSelectedCityChanged(
 231                          preferredCityString);
 232                    } // end else if
 233                 } // end method onClick
 234              }); // end OnClickListener
 235           // set the AlertDialog's negative Button
 236           builder.setNegativeButton(resources.getString(
 237              R.string.city_dialog_cancel),
 238              new DialogInterface.OnClickListener()
 239              {
 240                 // called when the "No" Button is clicked
 241                 public void onClick(DialogInterface dialog, int id)
 242                 {
 243                    dialog.cancel(); // dismiss the AlertDialog
 244                 } // end method onClick
 245              }); // end OnClickListener
 246  
 247           builder.create().show(); // display the AlertDialog
 248           return true;
 249        } // end citiesOnItemLongClickListener
 250     }; // end OnItemLongClickListener
 251  


Fig. 14.31. Implementing interface OnItemLongClickListener.

CitiesFragment Methods onSaveInstanceState, addCity and onListItemClick

Method onSaveInstanceState (Fig. 14.32) saves the position of the CitiesFragment’s currently selected item. The addCity method (Lines 263–273) is used by the WeatherViewerActivity to add new cities to the ListView. We add the new String to our ArrayAdapter then sort the Adapter’s items alphabetically. If the boolean parameter select is true, we pass the city name to the CitiesListChangeListener’s onSelectedCityChanged method so the WeatherViewerActivity will display the corresponding forecast.


 252     // save the Fragment's state
 253     @Override
 254     public void onSaveInstanceState(Bundle outStateBundle)
 255     {
 256        super.onSaveInstanceState(outStateBundle);
 257        
 258        // save current selected city to the Bundle
 259        outStateBundle.putInt(CURRENT_CITY_KEY, currentCityIndex);
 260     } // end onSaveInstanceState
 261  
 262     // add a new city to the list
 263     public void addCity(String cityNameString, boolean select)
 264     {
 265        citiesArrayAdapter.add(cityNameString);
 266        citiesArrayAdapter.sort(String.CASE_INSENSITIVE_ORDER);
 267  
 268        if (select) // if we should select the new city
 269        {
 270           // inform the CitiesListChangeListener
 271           citiesListChangeListener.onSelectedCityChanged(cityNameString);
 272        } // end if
 273     } // end method addCity
 274  
 275     // responds to a ListView item click
 276     @Override
 277     public void onListItemClick(ListView l, View v, int position, long id)
 278     {
 279        // tell the Activity to update the ForecastFragment
 280        citiesListChangeListener.onSelectedCityChanged(((TextView)v).
 281           getText().toString());
 282        currentCityIndex = position; // save current selected position
 283     } // end method onListItemClick
 284  } // end class CitiesFragment


Fig. 14.32. CitiesFragment methods onSaveInstanceState, addCity and onListItemClick.

Method onListItemClick (lines 276–283) responds to clicks on the ListView’s items. We pass the selected item’s city name to our CitiesListChangeListener’s onSelectedCityChanged method to inform the WeatherViewerActivity of the new selection, then store the index of the selected list item in currentCityIndex.

14.5.3. Class AddCityDialogFragment

Class AddCityDialogFragment (Fig. 14.33) allows the user to enter a ZIP code to add a new city to the favorite city list. The DialogFinishedListener interface (lines 19–23) is implemented by class WeatherViewerActivity (Fig. 14.23) so the Activity can receive the information that the user enters in the AddCityDialogFragment. Interfaces are commonly used in this manner to communicate information from a Fragment to a parent Activity. The DialogFragment has an EditText in which the user can enter a ZIP code, and a CheckBox that the user can select to set the new city as the preferred one.


  1   // AddCityDialogFragment.java
  2   // DialogFragment allowing the user to enter a new city's ZIP code.
  3   package com.deitel.weatherviewer;
  4   
  5   import android.app.DialogFragment;
  6   import android.os.Bundle;
  7   import android.view.LayoutInflater;
  8   import android.view.View;
  9   import android.view.View.OnClickListener;
 10   import android.view.ViewGroup;
 11   import android.widget.Button;
 12   import android.widget.CheckBox;
 13   import android.widget.EditText;
 14   
 15   public class AddCityDialogFragment extends DialogFragment
 16      implements OnClickListener
 17   {
 18      // listens for results from the AddCityDialog
 19      public interface DialogFinishedListener
 20      {
 21         // called when the AddCityDialog is dismissed
 22         void onDialogFinished(String zipcodeString, boolean preferred);
 23      } // end interface DialogFinishedListener
 24         
 25      EditText addCityEditText; // the DialogFragment's EditText
 26      CheckBox addCityCheckBox; // the DialogFragment's CheckBox
 27   
 28      // initializes a new DialogFragment
 29      @Override
 30      public void onCreate(Bundle bundle)
 31      {
 32         super.onCreate(bundle);
 33   
 34         // allow the user to exit using the back key
 35         this.setCancelable(true);
 36      } // end method onCreate
 37      
 38      // inflates the DialogFragment's layout
 39      @Override
 40      public View onCreateView(LayoutInflater inflater, ViewGroup container,
 41         Bundle argumentsBundle)                                            
 42      {
 43         // inflate the layout defined in add_city_dialog.xml
 44         View rootView = inflater.inflate(R.layout.add_city_dialog, container,
 45            false);
 46      
 47         // get the EditText
 48         addCityEditText = (EditText) rootView.findViewById(
 49            R.id.add_city_edit_text);
 50      
 51         // get the CheckBox
 52         addCityCheckBox = (CheckBox) rootView.findViewById(
 53            R.id.add_city_checkbox);
 54         
 55         if (argumentsBundle != null) // if the arguments Bundle isn't empty
 56         {
 57            addCityEditText.setText(argumentsBundle.getString(
 58               getResources().getString(
 59                  R.string.add_city_dialog_bundle_key)));
 60         } // end if
 61   
 62         // set the DialogFragment's title
 63         getDialog().setTitle(R.string.add_city_dialog_title);
 64   
 65         // initialize the positive Button
 66         Button okButton = (Button) rootView.findViewById(
 67            R.id.add_city_button);
 68         okButton.setOnClickListener(this);
 69         return rootView; // return the Fragment's root View
 70      } // end method onCreateView
 71      
 72      // save this DialogFragment's state
 73      @Override
 74      public void onSaveInstanceState(Bundle argumentsBundle)
 75      {
 76         // add the EditText's text to the arguments Bundle
 77         argumentsBundle.putCharSequence(getResources().getString(
 78            R.string.add_city_dialog_bundle_key),
 79            addCityEditText.getText().toString());
 80         super.onSaveInstanceState(argumentsBundle);
 81      } // end method onSaveInstanceState
 82   
 83      // called when the Add City Button is clicked
 84      @Override
 85      public void onClick(View clickedView)
 86      {
 87         if (clickedView.getId() == R.id.add_city_button)
 88         {
 89            DialogFinishedListener listener =
 90               (DialogFinishedListener) getActivity();
 91            listener.onDialogFinished(addCityEditText.getText().toString(),
 92               addCityCheckBox.isChecked() );
 93            dismiss(); // dismiss the DialogFragment
 94         } // end if
 95      } // end method onClick
 96   } // end class AddCityDialogFragment


Fig. 14.33. Class AddCityDialogFragment.

Overriding Method onCreate

We override onCreate (lines 29–36) to call DialogFragment’s setCancelable method. This allows the user to dismiss the DialogFragment using the device’s back key.

Overriding Method onCreateView

The DialogFragment’s layout is inflated in method onCreateView (lines 39–70). Lines 44–53 inflate the layout defined in add_city_dialog.xml then retrieve the DialogFragment’s EditText and Checkbox. If the user rotates the device while this dialog is displayed, the argumentsBundle contains any text the user entered into the EditText. This allows the DialogFragment to be rotated without clearing the EditText.

Overriding Method onCreate

Method onSaveInstanceState (lines 73–81) saves the current contents of the EditText allowing the Fragment to be restored with the same text in the future. We call the given argumentBundle’s putCharSequence method to save the text in the Bundle.

Overriding Method onCreate

We add the new city to the list and dismiss the AddCityDialogFragment in the onClick method (lines 84–95), which is called when the user clicks the Fragment’s Button. We pass the EditText’s text and the CheckBox’s checked status to our DialogFinishedListener’s onDialogFinished method. DialogFragment’s dismiss method is called to remove this Fragment from the Activity.

14.5.4. Class ForecastFragment

The ForecastFragment abstract class (Fig. 14.34) extends Fragment and provides the abstract method getZipcode that returns a ZIP code String. Class WeatherViewerActivity uses subclasses of ForecastFragment named SingleForecastFragment (Section 14.5.5) and FiveDayForecastFragment (Section 14.5.8) to display the current weather conditions and five-day forecast, respectively. Class WeatherViewerActivity uses getZipcode to get the ZIP code for the weather information displayed in each type of ForecastFragment.


  1   // ForecastFragment.java
  2   // An abstract class defining a Fragment capable of providing a ZIP code.
  3   package com.deitel.weatherviewer;
  4   
  5   import android.app.Fragment;
  6   
  7   public abstract class ForecastFragment extends Fragment
  8   {
  9      public abstract String getZipcode();
 10   } // end class ForecastFragment


Fig. 14.34. Class ForecastFragment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset