Weather Widget

W

The purpose of this tutorial is to create a widget for the Weather Forecast application which has been made in Weather Forecast tutorial. You can continue that tutorial to add a home screen widget to your Weather Forecast application.

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.

CREATE A NEW PROJECT

  • Start Android Studio and create a new project
  • Include Kotlin support
  • Set Phone and Tablet target and select newest API
  • Use Empty Activity template
  • Configure Activity as default settings

Declaring an App Widget in the Manifest

First, declare the AppWidgetProvider class inside Application element in your application’s AndroidManifest.xml file.

<receiver android:name="WeatherAppWidgetProvider">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data android:name="android.appwidget.provider"
             android:resource="@xml/weather_appwidget_info" />
</receiver>

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

Adding the 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.

Define the AppWidgetProviderInfo object in an XML resource using a single <appwidget-provider> element and save it in the project’s res/xml/ folder.

Create a new xml folder and weather_appwidget_info.xml file inside it. After that, add a below code inside it.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:minWidth="160dp"
  android:minHeight="40dp"
  android:updatePeriodMillis="86400000"
  android:initialLayout="@layout/weather_appwidget"
  android:resizeMode="horizontal|vertical"
  android:widgetCategory="home_screen">
</appwidget-provider>

Widget width will be now 3×1 squares in the home screen. It will use weather_appwidget 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.

Creating the 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.

Create a new layout file weather_appwidget.xml for displaying weather forecast. Use your own imagination to layout UI elements. Name your elements: cityTextViewcondTextViewtempTextViewtimeTextView and iconImageView.

Using the AppWidgetProvider Class

WeatherWidgetProvider

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.

Create a WeatherAppWidgetProvider class, which extends from the AppWidgetProvider class.

class WeatherAppWidgetProvider : AppWidgetProvider() {

} 

After that, add your API key and links to OpenWeather web site to WeatherAppWidgetProvider class. Those will be used later, when a data will be loaded with Volley.

// 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.

Add an onUpdate function to loop through each of the App Widget that belongs to this provider.

override fun onUpdate(
  context: Context,
  appWidgetManager: AppWidgetManager,
  appWidgetIds: IntArray
) {
  super.onUpdate(context, appWidgetManager, appWidgetIds)

  // Perform this loop procedure for each App Widget that belongs to this provider
  appWidgetIds.forEach { appWidgetId ->;
    // continue coding here...      
  }
}

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.

// Create an Intent to launch MainActivity
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)

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

// Load weather forecast
loadWeatherForecast("Jyväskylä", context, views, appWidgetId, appWidgetManager)

You need to pass context, AppWidgetID and appWidgetManager to loadWeatherForecast function to update widget.

Load weather forecast

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

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.

// JSON object request with Volley
val jsonObjectRequest = JsonObjectRequest(
  Request.Method.GET, url, null, Response.Listener<JSONObject> { 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")
    }
  },
  Response.ErrorListener { 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 Weather Forecast tutorial.

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

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

Get cityconditiontemperaturetime and icon url from the response data.

// 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.

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.

// 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.

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

Add Widget to the 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.

Refresh weather forecast

Vector Asset

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.

Pending Intent

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.

// create intent
val refreshIntent = Intent(context, WeatherAppWidgetProvider::class.java)
refreshIntent.action = "com.example.weatherapp.REFRESH"
refreshIntent.putExtra("appWidgetId", appWidgetId)
// create pending intent
val refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT)
// set pending intent to refresh image view
views.setOnClickPendingIntent(R.id.refreshImageView, refreshPendingIntent)

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

Receive custom message in Manifest

You need to edit project manifest intent filter add add fi.jamk.weatherapp.REFRESH custom action.

<receiver android:name="WeatherAppWidgetProvider" >
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    <action android:name="com.example.weatherapp.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.

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.weatherapp.REFRESH") {
    // get manager
    val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext)
    // get views
    val views = RemoteViews(context.packageName, R.layout.weather_appwidget)
    // 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.

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

Add comment

Pasi Manninen

Pasi Manninen

Pasi is a mobile and web application developer. Currently working as a senior lecturer in JAMK University of Applied Sciences. Pasi has programming experience over many decades and has taught dozens of courses since the 90's.

Recent Posts

Follow Me