Writing a Weather App for Android: The Code

I’m currently writing a basic weather app for Android using OpenWeatherMap. Thus far, I’ve:

Now it’s time to hook up the weather manager API to the UI, displaying the proper weather for my choice of location. This is all done within the Activity Java class – in my case, it’s MainActivity, but you can change this name if you need to. You just have to tell the app what Activity is loaded first. The current MainActivity is remarkably small:

package com.shellmonger.weatherapp;

import android.support.v7.app.AppCompatActivity;
import android.graphics.Typeface;
import android.widget.TextView;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    TextView weatherIcon;
    Typeface weatherFont;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        weatherFont = Typeface.createFromAsset(getAssets(), "fonts/weathericons-regular-webfont.ttf");
        weatherIcon = (TextView)findViewById(R.id.weather_icon);
        weatherIcon.setTypeface(weatherFont);
    }
}

The only thing I’ve added thus far is the code to set the font of the weather icon area to be the weather icon font. There are two methods that should be in every activity – the onCreate() method is called when the activity is being initialized, so it’s a great place to set up the click handlers, call the initial data loads, and so on. The other method (which is optional) is the onPause() method, which is called when the activity is going out of scope.

In my case, I want to call my weather manager API and then populate the UI with the data that I receive. This brings up the biggest thing to remember from this post. You can’t do network interactions on the UI thread. This is something that catches me out a lot. Instead, you have to do the network interaction on an async thread. The easiest way to do that is to use an AsyncTask class. Let’s take a look at the first version of the AsyncTask for this project:

    class GetWeatherAsyncTask extends AsyncTask<String,Void,WeatherResponse> {
        @Override
        protected WeatherResponse doInBackground(String... cities) {
            try {
                WeatherManager manager = new WeatherManager();
                return manager.getWeatherByCityName(cities[0]);
            } catch (WeatherException ex) {
                return null;
            }
        }

        @Override
        protected void onPostExecute(WeatherResponse response) {
            if (response != null) {
                // Do something here
            }
        }
    }

AsyncTasks can be in other files as a standalone class or embedded within an activity. Either way works. In my experience, the UI modifications are done within the onPostExecute() method, and so the AsyncTask is generally embedded with the activity that is handling it.

Now that I have a task to get the data, I’m going to need some method of kicking it off. This is done in the onCreate() method of my activity:

        GetWeatherAsyncTask task = new GetWeatherAsyncTask();
        task.execute(new String[] { "Seattle,WA" });

So now, my app will call the OpenWeatherMap API when it starts up, but it won’t actually display anything. To do that bit, I need to fill in the information in the onPostExecute() method of the async task:

        @Override
        protected void onPostExecute(WeatherResponse response) {
            if (response != null) {
                SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
                
                c_city.setText(response.getCityName() + ", " + response.getSysInfo().getCountry());
                c_details.setText(response.getWeatherConditions()[0].getLongDescription());
                double temp = response.getMain().getTemperature().getCurrentValue();
                double convertedTemp = temp * (9.0/5.0) - 459.67;
                c_temperature.setText(String.format("%.2f°F", convertedTemp));
                c_updated.setText(formatter.format(response.getTimestamp()));
            }
        }

The API gives me the temperature in Kelvin, so I have to do a calculation to get Fahrenheit. The values of c_city, c_details, c_temperature and c_updated are converted to simple text. Those variables are initialized in the onCreate() method of the activity:

        // Get the ID of each field that we want to modify
        c_city = (TextView)findViewById(R.id.city_field);
        c_details = (TextView)findViewById(R.id.details_field);
        c_temperature = (TextView)findViewById(R.id.current_temperature_field);
        c_updated = (TextView)findViewById(R.id.updated_field);
        weatherIcon = (TextView)findViewById(R.id.weather_icon);

That leaves only the weather icon to update. The response.getWeatherConditions()[0].getIcon() can only have certain values and those are listed in the API docs. I need to convert the list into something I can use, using a translation table:

        private String convertWeather(String iconCode) {
            boolean isDay = iconCode.charAt(2) == 'd';
            String weatherIcon = "f07b";
            int icon = Integer.parseInt(iconCode.substring(0,2), 10);

            switch (icon) {
                case 1:
                    weatherIcon = isDay ? "f00d" : "f02e";
                    break;
                case 2:
                case 3:
                case 4:
                    weatherIcon = isDay ? "f002" : "f086";
                    break;
                case 9:
                    weatherIcon = isDay ? "f009" : "f029";
                    break;
                case 10:
                    weatherIcon = isDay ? "f008" : "f028";
                    break;
                case 11:
                    weatherIcon = isDay ? "f005" : "f025";
                    break;
                case 13:
                    weatherIcon = isDay ? "f00a" : "f02a";
                    break;
                case 50:
                    weatherIcon = "&#xf014";
                    break;
            }
            return  "&#x" + weatherIcon + ";";
        }

This can then be used by the onPostExecute() method to fill in the weatherIcon field:

String weatherCode = convertWeather(response.getWeatherConditions()[0].getIcon());
// This is for API versions < 24
weatherIcon.setText(Html.fromHtml(weatherCode));

// This is for API versions >= 24
// weatherIcon.setText(Html.fromHtml(weatherCode, Html.FROM_HTML_MODE_LEGACY));

Note that there is a breaking change here depending on your minimum API level. I like to use API level 19 as my minimum as that captures ~75% of the Android market. As your minimum API level increases, the less devices will be able to use your app.

The final thing to do is to understand when the app is updating. I could use a progress bar or spinner. I like to keep things simple, so I’ll replace the updated field with the text “Updating” when it is updating. I can do this in the GetWeatherAsyncTask class simply enough by using an override of the onPreExecute() method.

Missing INTERNET feature?

There is always something more to do, it seems, with Android. If I run this app now, it will show “Updating…” and then nothing else. If I put a breakpoint in the doInBackground() method, it will show that the HTTP GET failed with this error:

The INTERNET capability needs to be added to the app. You can do this by editing the AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.shellmonger.weatherapp">

    <uses-permission android:name="android.permission.INTERNET"/>
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

A strange thing here – there is no “IDE” method to edit the manifest, unlike – say – Visual Studio. I find this strange because it’s a relative easy thing to do, it’s a common task, and it ensures that the file is correct.

What Cloud APIs fail

One of the problems I bumped into is what is the failure mode when the cloud API decides you are too busy? This can be fairly common in development. I do a run, realize something is wrong, correct it and then re-run. This can result in hundreds of API calls, which is something that the cloud APIs protect against, especially for non-paying accounts.

The answer is… wait. Walk away from the keyboard for an hour, and come back later. Alternatively, check out the failure mode. Does the API just not respond (resulting in a timeout?) . Then maybe develop using a mock data service (such as we discussed previously when testing the API service). Does the API produce dummy data? Then ignore the fact that the actual data is incorrect and concentrate on developing your app and fixing the bugs.

Whichever way you do it, be thankful that there are services out there like OpenWeatherMap that give you access to their API for free.

Other Things To Do?

The icon selection in the weather icon font is fairly extensive and I only use a portion of it. One of the things I could do is to use more of the icon set. Perhaps winds greater than 10mph are classified as “windy”, or I use more of the cloudy options to really say what is going on.

There is also more information that I can place in the display. Do I want pressure? Perhaps an arc that shows how far between sunset and sunrise the day is? These all require more advanced layout features and extra code, but the data is available to do this.

What’s Next

This is nice, but it isn’t finished yet. I want to add a button to refresh the data and some settings so that my users can specify the city of their choice. Until the next post, you can check out the code so far on my GitHub repository.