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 Fragment
s 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.
14.2 Test-Driving the Weather Viewer App
14.4 Building the App’s GUI and Resource Files
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.1 Class WeatherViewerActivity
14.5.3 Class AddCityDialogFragment
14.5.5 Class SingleForecastFragment
14.5.8 Class FiveDayForecastFragment
14.5.9 Class ReadFiveDayForecastTask
Self-Review Exercises | Answers to Self-Review Exercises | Exercises
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.
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).
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.
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.
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.
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.
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.
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.
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 DialogFragment
s. 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
Like an Activity
, each Fragment
has a life cycle—we’ll discuss the Fragment
life cycle methods as we encounter them. Fragment
s 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 Fragment
s. The parent Activity
uses a FragmentManager
(package android.app
) to manage the Fragment
s. A FragmentTransaction
(package android.app
) obtained from the FragmentManager
allows the Activity
to add, remove and transition between Fragment
s.
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 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.
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.
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 WidgetIt’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.
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
.
Intent
s and ReceiversThe 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.
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.
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>
WeatherViewerActivity
’s main.xml
LayoutThe 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 ForecastFragment
s. By including this placeholder we define the size and location of the area in which the ForecastFragment
s will appear in the Activity
. The WeatherViewerActivity
swaps between ForecastFragment
s in this location using FragmentTransaction
s.
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>
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 array
s 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>
WeatherViewerActivity
’s actionmenu.xml
Menu LayoutThe 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>
WeatherProvider
App Widget Configuration and LayoutThe 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"/>
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
This app consists of 11 classes that are discussed in detail in Sections 14.5.1—14.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 ForecastFragment
s (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.
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
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 Fragment
s—in this case, we get the CitiesFragment
. The FragmentManager
is also available to any of the Activity
’s Fragment
s. 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
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
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
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
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
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
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
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
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
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). FragmentTransaction
s are used to add, remove and replace Fragment
s, 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
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
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
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
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
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
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 Tab
s. 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 Tab
s to the ActionBar
with ActionBar
’s addTab
method. We create two Tab
s, 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
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
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 InterfaceFig. 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
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
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
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
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
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
.
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
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.
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
.
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
.
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
.
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