Android development blog
Tutorials about Android dev topics

Android Weather app Tutorial: Step by Step guide (Part 2)

Topics covered

Android Weather App

Yahoo! Weather Client tutorial

XML Parser

Volley

Android app tuturial


I've released a new WeatherLib for Android that simplifies the app coding helping you in many app details. If you are interested give a look here

In this post you will find a complete tutorial explaining how to build an Android app. The goal of this post is creating a Weather App that will use Yahoo! Weather as data provider. This post covers the most important aspects, we should consider when building an app. It will explain how to use Yahoo! Weather API to retrieve XML weather data and how to parse it to extract the information.
In the last post, we discovered how we can retrieve the woeid from the city name. This information is very important because we can use it to get weather data. At the end of this post you will create a full working app that looks like:
android_weather_app
android_weather_app_settings_1 android_weather_app_settings

and is published on the market so that you can download it and play with it.


App structure

We want to create an app with two different areas:
  • Weather information
  • App Settings
The first area is where the app shows the current weather information retrieved using Yahoo! Weather API, while the second area, called App Settings, is where we can configure our app, find the city woeid and the system measure unit. The pictures below show how the settings area should be:
android_weather_app_settings_1
As first step, we will create a preference activity, where an user can configure the weather app. In this case we can create a class, called WeatherPreferenceActivity that extends PreferenceActivity, and set preference layout:
public class WeatherPreferenceActivity extends PreferenceActivity  {
   @Override
    public void onCreate(Bundle Bundle) {
        super.onCreate(Bundle);
        getActionBar().setDisplayHomeAsUpEnabled(true);
        String action = getIntent().getAction();

        addPreferencesFromResource(R.xml.weather_prefs);
...
}

To create the preference layout, we can use an XML file under /res/xml and we call it weather_prefs.xml. It looks like the XML shown below:
<PreferenceScreen  xmlns:android="http://schemas.android.com/apk/res/android">
  <PreferenceCategory android:title="@string/loc_title">
      <Preference android:title="@string/pref_location_title"
                  android:key="swa_loc">
          <intent android:targetPackage="com.survivingwithandroid.weather"
                android:targetClass="com.survivingwithandroid.weather.settings.CityFinderActivity"
                  />
      </Preference>
  </PreferenceCategory>

    <PreferenceCategory android:title="@string/pref_unit_title">
        <ListPreference android:key="swa_temp_unit"
            android:title="@string/temp_title"
            android:entryValues="@array/unit_values"
            android:entries="@array/unit_names"
            android:defaultValue="c" />

    </PreferenceCategory>

</PreferenceScreen>

You can notice we dived the setting screen in two different sections (there are two PreferenceScreen tag). At the line 2 to 7 we start another Activity as the user select this option because we have to give to the user the chance to select the city name and resolve it in the woeid that we will use later.  To start another activity inside a PreferenceCategory we use an Intent, passing the package name and class name. The second section is used to select the measure unit system, if the user uses °C that the system will be the metric system. It is a good practice to show to the user the current values, so that in the onCreate method of WeatherPreferenceActivity we add these lines of code:
  SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        // We set the current values in the description
        Preference prefLocation = getPreferenceScreen().findPreference("swa_loc");
        Preference prefTemp = getPreferenceScreen().findPreference("swa_temp_unit");

        prefLocation.setSummary(getResources().getText(R.string.summary_loc) + " " + prefs.getString("cityName", null) + "," + prefs.getString("country", null));

        String unit =  prefs.getString("swa_temp_unit", null) != null ? "°" + prefs.getString("swa_temp_unit", null).toUpperCase() : "";
        prefTemp.setSummary(getResources().getText(R.string.summary_temp) + " " + unit);

We used at line 1 the SharedPreference class to hold the app settings.

Yahoo! Weather client


Now we have built the activity that enables users to configure the app, we can focus our attention on how to build the client that retrieve the weather information using Yahoo! Weather client. We create a new class called YahooClient where we will implement the logic to connect the remote server and retrieve the data.

The first step is creating the class structure that will hold the information we retrieve from XML received from the remote server. This class structure maps somehow the XML received from the server, so we can suppose we have something like the pic shown below:

y_class

The Weather class, is the class that will be returned and passed back to activity to display the information. We can create a static method called getWeather that uses Volley lib to connect to the remote server. We have to create the url that will be called:
http://weather.yahooapis.com/forecastrss?w=woeid&u=unit

Now we have the url we can implement the client:
   public static void getWeather(String woeid, String unit, RequestQueue rq, final WeatherClientListener listener) {
        String url2Call = makeWeatherURL(woeid, unit);
        Log.d("SwA", "Weather URL ["+url2Call+"]");
        final Weather result = new Weather();
        StringRequest req = new StringRequest(Request.Method.GET, url2Call, new Response.Listener<String>() {
            @Override
            public void onResponse(String s) {
                parseResponse(s, result);
                listener.onWeatherResponse(result);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {

            }
        });

        rq.add(req);
    }

At line 5 we create the HTTP request, using GET method, and wait for response. As you already know (if not look at this post explaining how to use Volley) we have two listener to implement one that handles the incoming response and another one that handles errors that may occur. At the moment we want just to handle the response (see line 8,9), where first we parse the XML and then we notify the result the caller (line 9). We define our listener:
 public static interface WeatherClientListener {
        public void onWeatherResponse(Weather weather);
    }

Finally, at line 18 we add the request to the queue.

Parsing XML is very simple, we have in input a String, that holds the XML,  and we look for the tag we are interested on, and create the our pojo (Weather) . The parser is shown below:
private static Weather parseResponse (String resp, Weather result) {
    Log.d("SwA", "Response ["+resp+"]");
    try {
        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
        parser.setInput(new StringReader(resp));

        String tagName = null;
        String currentTag = null;

        int event = parser.getEventType();
        boolean isFirstDayForecast = true;
        while (event != XmlPullParser.END_DOCUMENT) {
            tagName = parser.getName();

            if (event == XmlPullParser.START_TAG) {
                if (tagName.equals("yweather:wind")) {
                    ...
                }
                else if (tagName.equals("yweather:atmosphere")) {
                    ... 
                }
                else if (tagName.equals("yweather:forecast")) {
                    ...
                }
                else if (tagName.equals("yweather:condition")) {
                    ...
               }
                else if (tagName.equals("yweather:units")) {
                   ...
                }
                else if (tagName.equals("yweather:location")) {
                    ...
                }
                else if (tagName.equals("image"))
                    currentTag = "image";
                else if (tagName.equals("url")) {
                    if (currentTag == null) {
                        result.imageUrl = parser.getAttributeValue(null, "src");
                    }
                }
                else if (tagName.equals("lastBuildDate")) {
                   currentTag="update";
                }
                else if (tagName.equals("yweather:astronomy")) {
                ...
             }

            }
            else if (event == XmlPullParser.END_TAG) {
                if ("image".equals(currentTag)) {
                   currentTag = null;
                }
            }
            else if (event == XmlPullParser.TEXT) {
                if ("update".equals(currentTag))
                    result.lastUpdate = parser.getText();
            }
            event = parser.next();
        }

    }
    catch(Throwable t) {
        t.printStackTrace();
    }

    return result;
}



App navigation and ActionBar


The next step is building the app navigation structure. We already know we have two activities: one that shows current weather condition and another one used for app settings. We can use the well-know actionbar pattern to handle navigation between these activities. We can create (if not exist) under /res/menu a file called main.xml. This file will contain all the menu item we want to show to the user:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.survivingwithandroid.weather.MainActivity" >

    <item android:id="@+id/action_donate"
        android:title="@string/action_donate"
        android:orderInCategory="100"
        app:showAsAction="never"
        android:icon="@android:drawable/ic_menu_manage"/>

    <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"
        android:icon="@android:drawable/ic_menu_manage"/>

    <item android:id="@+id/action_refresh"
        android:title="@string/action_refresh"
        android:orderInCategory="50"
        android:icon="@drawable/ic_menu_refresh"
        android:showAsAction="ifRoom"/>

    <item android:id="@+id/action_share"
        android:title="@string/action_share"
        android:orderInCategory="50"
        android:icon="@android:drawable/ic_menu_share"
        android:showAsAction="ifRoom"/>


</menu>

As result we have:

android_actionbar_menu_item

and  in the MainActivity.java we have:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
   
    int id = item.getItemId();
    if (id == R.id.action_settings) {
        Intent i = new Intent();
        i.setClass(this, WeatherPreferenceActivity.class);
        startActivity(i);
    }
    else if (id == R.id.action_refresh) {
        refreshItem = item;
        refreshData();
    }
    else if (id == R.id.action_share) {
        String playStoreLink = "https://play.google.com/store/apps/details?id=" +
                getPackageName();

        String msg = getResources().getString(R.string.share_msg) + playStoreLink;
        Intent sendIntent = new Intent();
        sendIntent.setAction(Intent.ACTION_SEND);
        sendIntent.putExtra(Intent.EXTRA_TEXT, msg);
        sendIntent.setType("text/plain");
        startActivity(sendIntent);
    }
    else if (id == R.id.action_donate) {
        SwABillingUtil.showDonateDialog(this, mHelper, this);
    }
    return super.onOptionsItemSelected(item);
}

To provide Up Navigation, we add this line of code to onCreate method of WeatherPreferenceActivity:
getActionBar().setDisplayHomeAsUpEnabled(true);

At the same time, we want that when user selects a city in CityFinderActivity we come back to the preference screens so we add :
 NavUtils.navigateUpFromSameTask(CityFinderActivity.this);

 


MainActivity and App layout


The last step is setting up the layout of the MainActivity showing all the information we retrieved from remote server. In this case we can define a simple layout that looks like the one shown below:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
   tools:context="com.survivingwithandroid.weather.MainActivity$PlaceholderFragment">

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

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tempLyt"
        android:layout_below="@id/location"
        android:layout_centerHorizontal="true">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            style="@style/textBig"
            android:id="@+id/temp"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="3dp"
            android:layout_alignLeft="@id/temp"
            android:layout_alignRight="@id/temp"
            android:id="@+id/lineTxt"
            android:layout_below="@id/temp"
            android:layout_marginTop="0dp" />

        <ImageView
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginLeft="10dp"
            android:id="@+id/imgWeather"
            android:layout_toRightOf="@id/temp"
            android:layout_alignTop="@id/temp"
            />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/tempUnit"
            android:layout_alignBaseline="@id/temp"
            android:layout_toRightOf="@id/temp"
            android:layout_alignStart="@id/imgWeather"
            style="@style/textSmall"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/descrWeather"
            android:layout_below="@id/imgWeather"
            android:layout_toRightOf="@id/temp"
            android:layout_alignStart="@id/tempUnit"
            style="@style/textSmall"/>


    </RelativeLayout>


    <!-- Here the current weather data -->

    <!-- Temperature data -->
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/tempIcon"
        android:src="@drawable/temperature"
        android:layout_below="@id/tempLyt"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tempMin"
        android:layout_toRightOf="@id/tempIcon"
        android:layout_alignTop="@id/tempIcon"
        android:layout_marginTop="12dp"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tempMax"
        android:layout_toRightOf="@id/tempMin"
        android:layout_alignBaseline="@id/tempMin"
        android:layout_marginLeft="10dp"/>
    <!-- End temp Data -->

    <!-- Wind data -->
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/windIcon"
        android:src="@drawable/wind"
        android:layout_below="@id/tempIcon"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/windSpeed"
        android:layout_toRightOf="@id/windIcon"
        android:layout_alignTop="@id/windIcon"
        android:layout_marginTop="12dp"
        android:layout_alignStart="@id/tempMin"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/windDeg"
        android:layout_toRightOf="@id/windSpeed"
        android:layout_alignBaseline="@id/windSpeed"
        android:layout_marginLeft="10dp"/>
    <!-- End wind Data -->

   <!-- Humidity -->
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/humidityIcon"
        android:src="@drawable/humidity"
        android:layout_below="@id/windIcon"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/humidity"
        android:layout_toRightOf="@id/humidityIcon"
        android:layout_alignTop="@id/humidityIcon"
        android:layout_marginTop="12dp"
        android:layout_alignStart="@id/tempMin"
        android:layout_marginLeft="10dp"/>
    <!-- End Humidity Data -->

    <!-- Pressure data -->
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/pressureIcon"
        android:src="@drawable/pressure"
        android:layout_below="@id/humidityIcon"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/pressure"
        android:layout_toRightOf="@id/pressureIcon"
        android:layout_alignTop="@id/pressureIcon"
        android:layout_marginTop="12dp"
        android:layout_alignStart="@id/tempMin"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/pressureStat"
        android:layout_toRightOf="@id/pressure"
        android:layout_alignBaseline="@id/pressure"
        android:layout_marginLeft="10dp"/>
    <!-- End Pressure data -->

    <!-- Visibility -->
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/visibilityIcon"
        android:src="@drawable/eye"
        android:layout_below="@id/pressureIcon"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/visibility"
        android:layout_toRightOf="@id/visibilityIcon"
        android:layout_alignTop="@id/visibilityIcon"
        android:layout_marginTop="12dp"
        android:layout_alignStart="@id/tempMin"
        android:layout_marginLeft="10dp"/>
    <!-- End visibility -->

    <!-- Astronomy -->
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/sunIcon"
        android:src="@drawable/sun"
        android:layout_below="@id/visibilityIcon"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/sunrise"
        android:layout_toRightOf="@id/sunIcon"
        android:layout_alignTop="@id/sunIcon"
        android:layout_marginTop="12dp"
        android:layout_alignStart="@id/tempMin"
        android:layout_marginLeft="10dp"/>
    <ImageView
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:id="@+id/moonIcon"
        android:src="@drawable/moon"
        android:layout_below="@id/sunIcon"
        android:layout_marginTop="10dp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/sunset"
        android:layout_toRightOf="@id/moonIcon"
        android:layout_alignTop="@id/moonIcon"
        android:layout_marginTop="12dp"
        android:layout_alignStart="@id/tempMin"
        android:layout_marginLeft="10dp"/>

    <!-- End astronomy -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:text="@string/provider"
        style="@style/textVerySmall"
        />
</RelativeLayout>

The layout structure is shown below:

app_weather_layout_structure

This layout will be filled, at runtime, with the data extracted from XML.

Now in the MainActivity we simply call the YahooClient to retrieve data and coordinate the activities:
protected void onCreate(Bundle savedInstanceState) {
 ...
 refreshData();
}

private void refreshData() {

    if (prefs == null)
        return ;

    String woeid = prefs.getString("woeid", null);
    
    if (woeid != null) {
        String loc = prefs.getString("cityName", null) + "," + prefs.getString("country", null);
        String unit = prefs.getString("swa_temp_unit", null);
        handleProgressBar(true);

        YahooClient.getWeather(woeid, unit, requestQueue, new YahooClient.WeatherClientListener() {
            @Override
            public void onWeatherResponse(Weather weather) {
                // We update the view
                ..
                // We retrieve the image
                IWeatherImageProvider provider = new WeatherImageProvider();
                provider.getImage(code, requestQueue, new IWeatherImageProvider.WeatherImageListener() {
                    @Override
                    public void onImageReady(Bitmap image) {
                        weatherImage.setImageBitmap(image);
                    }
                });
                handleProgressBar(false);
            }
        });


    }
}

In refreshData method we simply retrieve the app setting stored in SharedPreferences (see line 11,14,15) and at line 18 we invoke the YahooClient method getWeather to retrieve the data. We have to remember that we call the HTTP URL in a background thread to avoid ANR problem, so we wait for the response using a listener (see line 20). When we get the response we update the view. Finally at line 25, we retrieve the image related to the weather condition.

Source code available @ github

4 comments:

  1. Nice tutorial! Looking forward to building my own weather app

    ReplyDelete
  2. Great tutorial, thanks for sharing it!

    ReplyDelete
  3. This is really a very good article, I was looking for this from long time, ThanksAndroid tutorial kitkat 4.4 with Source code

    ReplyDelete

Related Posts with Thumbnails