Writing a Weather App for Android: Refresh

In the past few posts, I’ve been writing a weather app for Android in the native language using the OpenWeatherMap API – Java. You can check out the prior posts here:

It’s a useful app at this point. It displays the data from my home city. However, I want to add some more detail to it. Firstly, it only displays the data once. I’d like it to refresh periodically, even if I’m not doing anything. Secondly, I want to force an update with a refresh button.

The Refresh Button

Let’s take each of these in turn. Firstly, let’s deal with the refresh button. I’ve already isolated all the code necessary to call the OpenWeatherMap API into an AsyncTask called GetWeatherAsyncTask. All I need to do is create a refresh button and wire up the button click to execute the async task, right? Well, that’s easy enough and there are enough recipes on the Internet for this. Firstly, add an ImageView to the activity_main.xml file:

    <ImageView
        android:clickable="true"
        android:id="@+id/refresh_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentTop="true"
        android:onClick="onRefreshClick"
        app:srcCompat="@android:drawable/ic_popup_sync" />

Then, wire up the click handler for the ImageView in the MainActivity.java class:

    public void onRefreshClick(View view) {
        GetWeatherAsyncTask task = new GetWeatherAsyncTask();
        task.execute(new String[] { "Seattle,US" });
    }

This will certainly work, but it has a few issues. Firstly, this will kick off a new task for every single click – irrespective of whether there is an update happening now or not. There is no rate limiting here, so mashing on the button will make you bump into the API rate limits of the OpenWeatherMap plan. There is also no indication as to whether you can click on the button or not. Obviously, I need to do some more work. Let’s start with handling the multiple requests. I’m going to add a field called updateInProgress to the MainActivity class. I’m going to set the field in the onPreExecute() and onPostExecute() methods in the GetWeatherAsyncTask class:

        @Override
        protected void onPreExecute() {
            c_updated.setText("Updating...");
            setUpdateInProgress(true);
        }

        @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) {
                try {
                    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()));
                    String weatherCode = convertWeather(response.getWeatherConditions()[0].getIcon());
                    // API Level < 24
                    weatherIcon.setText(Html.fromHtml(weatherCode));
                    // API Level 24+
                    // weatherIcon.setText(Html.fromHtml(weatherCode, Html.FROM_HTML_MODE_LEGACY));
                } catch (Exception ex) {
                    c_updated.setText("Formatting Failed");
                }
            } else {
                c_updated.setText("Fetch Failed");
            }
            setUpdateInProgress(false);
        }

I’m using setUpdateInProgress() rather than just setting the property because I also want to change the imageView according to the value of the updateInProgress:

        private void setUpdateInProgress(boolean v) {
            updateInProgress = v;
            c_refresh.setClickable(!v);
            c_refresh.setColorFilter(v ? Color.GRAY : Color.WHITE);
        }

Since the update is going to happen on startup, I don’t really need to do anything else. As a matter of course, however, I’ll set the initial color to white in the onCreate() method. With this code, the button will be clickable. Clicking on the button will initiate the async task; that turns the button gray (and disables the click), does the background task of downloading the new data, and then enables the click (turning the button white again). This solves two of my issues. I can’t kick off multiple background tasks and I have an indication of when an update is happening.

There are various ways of doing rate limiting. You could do it in the WeatherManager such that it caches the data and immediately returns the cache if not enough time has passed. You could also do it in the MainActivity. Instead of immediately enabling the button, kick off another AsyncTask that enables the button after a period of time.

Adding a Timer

The other thing I want to do is to add a timer action. The timer action should refresh the data every 5 minutes. The Timer object will create an async thread for me, so I can control it quite easily. First, create a Timer object and a TimerTask object within the onCreate() method of the Activity. Then schedule the trigger (in milliseconds):

        // Set up the timer
        refreshTimer = new Timer();
        refreshTimerTask = new TimerTask() {
            public void run() {
                onRefreshClick(null);
            }
        };
        refreshTimer.schedule(refreshTimerTask, 0L, 300000L);

Note that I don’t need to do the initial request any more. The refresh timer will do the first request for me immediately (that’s the 0L – meaning immediately). Thereafter, the refresh will happen as if I clicked on the refresh button every five minutes.

Except that this won’t work. That’s because only the UI thread is allowed to update the UI and the timer is running on a different thread. Instead, you have to create a Handler object in the UI thread to do the work, then pass a message to the Handler object to trigger it. Here is the updated code:

        // Set up the timer
        timerHandler = new Handler() {
            public void handleMessage(Message msg) {
                onRefreshClick(null);
            }
        };
        refreshTimer = new Timer();
        TimerTask refreshTimerTask = new TimerTask() {
            public void run() { timerHandler.obtainMessage(1).sendToTarget(); }
        };
        refreshTimer.schedule(refreshTimerTask, 0L, 300000L);

This time, the timer sends a message to the handler which runs in the UI thread. The handler will kick off the refresh, which updates the UI (because it is on the UI thread).

Wrap Up

There isn’t much more to do. The next time I visit this topic, I am going to talk about switching over the app to use my current location. Until then, you can find the code for this app on my GitHub repository.