Skip to content

Exercise 14 - Weather Widget

Project name : E14 Weather Widget

Purpose

In this exercise you will made a weather forecast widget using Android Studio and Kotlin programming language. Application will load weather forecast data from the Open Weather Map. Loaded weather forecast data will be shown in Android device home screen.

 

Widgets are an essential aspect of home screen customization. You can imagine them as “at-a-glance” views of an app’s most important data and functionality that is accessible right from the user’s home screen. Users can move widgets across their home screen panels and, if supported, resize them to tailor the amount of information within a widget to their preference.

Weather Forecast

Create a free account to OpenWeatherMap and get a API key. Look and learn Current weather data API example calls and get familiar with the returned forecast JSON data. You can access current weather data for any location on Earth including over 200,000 cities.

Learn

Check Build a App Widget material from the Android Developer site and get the basic knowledge of the App Widgets.

Create a new project

  • Start Android Studio and create a new project
  • Choose your project template as a Empty Activity Views
  • Name your project to E14 Weather Widget and include Kotlin support
  • Select the newest available API

Add Widget to your project

Select New > Widget > App Widget and add a new Widget to your project

!Choose project template

Click image to see it bigger!

Declare an App Widget in the Manifest

First, check how the WeatherAppWidget class is declared inside Application element in your application’s AndroidManifest.xml file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<receiver
  android:name=".WeatherAppWidget"
  android:exported="true">
  <intent-filter>
      <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data
      android:name="android.appwidget.provider"
      android:resource="@xml/weather_app_widget_info" />
</receiver>

The <receiver> element requires the android:name attribute, which specifies the AppWidgetProvider used by the App Widget. WeatherAppWidget class will be the widgets main class. The <intent-filter> element must include an <action> element with the android:name attribute. This attribute specifies that the AppWidgetProvider accepts the APPWIDGET_UPDATE broadcast. The <meta-data> element specifies the AppWidgetProviderInfo resource and now weather_app_widget_info will describe widgets metadata.

Add AppWidgetProviderInfo metadata

The AppWidgetProviderInfo defines the essential qualities of an App Widget. For example, its minimum layout dimensions, its initial layout resource, how often to update the App Widget, and (optionally) a configuration Activity to launch at create-time.

Check generated weather_app_widget_info.xml from your res/xml folder. Now it has a lot of "default" values inside and your can modify to your own purpose (graphics etc...).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="@string/app_widget_description"
  android:initialKeyguardLayout="@layout/weather_app_widget"
  android:initialLayout="@layout/weather_app_widget"
  android:minWidth="160dp"
  android:minHeight="40dp"
  android:previewImage="@drawable/example_appwidget_preview"
  android:previewLayout="@layout/weather_app_widget"
  android:resizeMode="horizontal|vertical"
  android:targetCellWidth="2"
  android:targetCellHeight="1"
  android:updatePeriodMillis="86400000"
  android:widgetCategory="home_screen" />

Widget width will be now 3×1 squares in the home screen. It will use weather_app_widget layout and it can be resized horizontally and vertically. Widget is now defined to be used only in a home screen.

You can also use a preview image which will be visible at the widgets menu and when a widget will be dragged to the home screen. There is also possibility to use a configuration activity, which will be launched automatically, when widget will be dropped to the home screen.

Create App Widget Layout

You must define an initial layout for your App Widget in XML and save it in the project’s res/layout/ folder. You can use Android UI designer to design your Widget. However, you must be aware that App Widget layouts are based on RemoteViews, which do not support every kind of layout or view widget. Check all of the available views and layouts from the Android Developer site: Creating the App Widget layout.

Tip

You can't use CoordinatorLayout, which is default layout in Android Studio projects.

Modify file weather_app_widget.xml for displaying weather forecast. Use your own imagination to layout UI elements. Name your elements: cityTextView, condTextView, tempTextView, timeTextView and iconImageView.

!Image 03

Use AppWidgetProvider class

WeatherAppWidget

The AppWidgetProvider class extends from the BroadcastReceiver as a convenience class to handle the App Widget broadcasts. The AppWidgetProvider receives only the event broadcasts which are relevant to the App Widget, like updated, deleted, enabled, and disabled events. In this tutorial you will need onUpdate and onReceive functions.

Check generated a WeatherAppWidget class, which extends from the AppWidgetProvider class.

1
2
3
class WeatherAppWidget : AppWidgetProvider() {

} 

Add your API key and links to OpenWeather web site to WeatherAppWidget class. Those will be used later, when a data will be loaded with Volley.

1
2
3
4
// example call is : https://api.openweathermap.org/data/2.5/weather?q=Jyväskylä&APPID=YOUR_API_KEY&units=metric
val API_LINK: String = "https://api.openweathermap.org/data/2.5/weather?q="
val API_ICON: String = "https://openweathermap.org/img/w/"
val API_KEY: String = "YOUR_API_KEY_HERE"

onUpdate function

The most important AppWidgetProvider callback is onUpdate() because it is called, when each App Widget is added to a host (unless you use a configuration Activity). You will need to register all of your application user interaction events inside this function.

Check an onUpdate() function which loops through each of the App Widget that belongs to this provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
override fun onUpdate(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetIds: IntArray
) {
    // There may be multiple widgets active, so update all of them
    for (appWidgetId in appWidgetIds) {
        // Continue coding here...
        updateAppWidget(context, appWidgetManager, appWidgetId)
    }
}

In this tutorial the MainActivity will be opened, when the end user has clicked the cityTextView text. You will need to use a PendingIntent to it. A Pending Intent specifies an action to take in the future. It lets you pass a future Intent to another application and allow that application to execute that Intent.

After that a weather forecast will be loaded with own made loadForecast function.

Add below code to inside above widget’s loop.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for (appWidgetId in appWidgetIds) {
  // Create an Intent to launch MainActivity
  val intent = Intent(context, MainActivity::class.java)
  val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

  // Get the layout for the App Widget and attach an on-click listener
  val views = RemoteViews(context.packageName, R.layout.weather_app_widget)
  views.setOnClickPendingIntent(R.id.cityTextView, pendingIntent)

  // Load weather forecast
  loadWeatherForecast("Jyväskylä", context, views, appWidgetId, appWidgetManager)
  // comment below line, we will update widget later inside ^ above function
  //updateAppWidget(context, appWidgetManager, appWidgetId)
}

Load Weather Forecast

Create loadWeatherForecast function, which will be used to load weather forecast from the OpenWeather. You should look exercise 15, if you haven’t done it, to understand the loading and parsing JSON data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private fun loadWeatherForecast(
  city:String, 
  context: Context, 
  views: RemoteViews, 
  appWidgetId: Int, 
  appWidgetManager: AppWidgetManager) 
{

  // URL to load forecast
  val url = "$API_LINK$city&APPID=$API_KEY&units=metric"

  // continue coding here...

}

After that use Volley to load JSON data from the OpenWeather. Remember install Volley to your project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// JSON object request with Volley
val jsonObjectRequest = JsonObjectRequest(
    Request.Method.GET, url, null, { response ->
        try {
            // load OK - parse data from the loaded JSON
            // **add parse codes here... described later**
        } catch (e: Exception) {
            e.printStackTrace()
            Log.d("WEATRHER", "***** error: $e")
        }
    },
    { error -> Log.d("ERROR", "Error: $error") })
// start loading data with Volley
val queue = Volley.newRequestQueue(context)
queue.add(jsonObjectRequest)

Volley will call Response.Listener<JSONObject> with JSON response data when a loading has finished successfully. You will need to parse JSON data to show it in the UI. You will need to check the structure of the loaded JSON from the OpenWeather API web site or exercise 15.

Use getJSONObject function to get main object with temperature. Use getJSONArray function to get weather array with main condition and weather icon.

1
2
3
val mainJSONObject = response.getJSONObject("main")
val weatherArray = response.getJSONArray("weather")
val firstWeatherObject = weatherArray.getJSONObject(0)

Get city, condition, temperature, time and icon url from the response data.

1
2
3
4
5
6
7
8
9
// city, condition, temperature
val city = response.getString("name")
val condition = firstWeatherObject.getString("main")
val temperature = mainJSONObject.getString("temp")+" °C"
// time
val weatherTime: String = response.getString("dt")
val weatherLong: Long = weatherTime.toLong()
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.YYYY HH:mm:ss")
val dt = Instant.ofEpochSecond(weatherLong).atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter).toString()

After that, set forecast text’s to textViews and load icon with Glide.

1
2
3
4
views.setTextViewText(R.id.cityTextView, city)
views.setTextViewText(R.id.condTextView, condition)
views.setTextViewText(R.id.tempTextView, temperature)
views.setTextViewText(R.id.timeTextView, dt)

You will need to use AppWidgetTarget class with Glide to get icon loaded to the home screen widget.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// AppWidgetTarget will be used with Glide - image target view
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.iconImageView, views, appWidgetId) {}
val weatherIcon = firstWeatherObject.getString("icon")
val url = "$API_ICON$weatherIcon.png"

Glide
  .with(context)
  .asBitmap()
  .load(url)
  .into(awt)

After that, you need to update widget to display a new content.

1
2
// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager.updateAppWidget(appWidgetId, views)

You can comment or delete template generated updateAppWidget function, it will not be used.

Add Widget to home screen

Test and run the application in the emulator or the device. Close your application and press&hold finger in the home screen to activate device menu. Select Widgets and scroll down to find Weather App widget. After that, select Widget and drag & drop it to home screen. Select Widget from the home screen and resize it if needed.

 

You can launch the MainActivity by pressing a city name in the widget.

Tip

Yes, UI is very ugly right now. Try to modify it look Cool! - Be Innovative!

Refresh Weather Forecast

Icon

Add a new refresh vector asset to your project and widgets layout. You can follow this guide to add a new Vector Asset to your project: Add multi-density vector graphics.

!Image 07

Pending Indent

Add a new pending intent to your refresh image in onUpdate function. In other words, it can be used to refresh a new weather forecast.

1
2
3
4
5
6
7
8
// create intent
val refreshIntent = Intent(context, WeatherAppWidget::class.java)
refreshIntent.action = "com.example.weatherwidget.REFRESH"
refreshIntent.putExtra("appWidgetId", appWidgetId)
// create pending intent
val refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
// set pending intent to refresh image view
views.setOnClickPendingIntent(R.id.refreshImageView, refreshPendingIntent)

Now a custom com.example.weatherwidget.REFRESH action will be send to WeatherAppWidgetProvider class with appWidgetId data when user is pressing the refresh image in the widget.

Receive a custom message in Manifest

You need to edit project manifest intent filter add add com.example.weatherwidget.REFRESH custom action.

1
2
3
4
5
6
7
8
<receiver android:name="WeatherAppWidget" android:exported="true">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    <action android:name="com.example.weatherwidget.REFRESH"/>
  </intent-filter>
  <meta-data android:name="android.appwidget.provider"
    android:resource="@xml/weather_appwidget_info" />
</receiver>

onReceive

Above custom action can be determined in onReceive function inside a WeatherAppWidgetProvider class. Add a following onReceive method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
override fun onReceive(context: Context, intent: Intent) {
  super.onReceive(context, intent)

  // got a new action, check if it is refresh action
  if (intent.action == "com.example.weatherwidget.REFRESH") {
    // get manager
    val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext)
    // get views
    val views = RemoteViews(context.packageName, R.layout.weather_app_widget)
    // get appWidgetId
    val appWidgetId = intent.extras!!.getInt("appWidgetId")
    // load data again
    loadWeatherForecast("Jyväskylä", context, views, appWidgetId, appWidgetManager)
  }

}

Now onReceive function will be called, when a refresh image will be clicked on the home screen.

Note

OpenWeather will update weather forecast within a few minutes. Wait a few minutes and click a refresh image to get your weather forecast updated.

Test and push

Test your application in emulator, take screenshots and commit/push your project and screenshots back to JAMKIT/GitLab. Remember move your exercise/issue ticket from Doing to In Review in Issues Board and write your learning comments to issue comments.