Weather Forecast

W

In this tutorial you will made a weather forecast application 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 the multiple fragments with ViewPager component.

OpenWeatherMap weather service is based on the VANE Geospatial Data Science platform for collecting, processing, and distributing information about our planet through easy to use tools and APIs.

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.

Create a new project

  • Start Android Studio and create a new project
  • Choose your project template as a Tabbed Activity
  • Name your project to WeatherApp and include Kotlin support
  • Select the newest available API

Tabbed Activity template is using a Swipe views, which allow you to navigate between sibling screens, such as tabs, with a horizontal finger gesture, or swipe. This navigation pattern is also referred to as horizontal paging. This tutorial teaches you how to create a tab layout with swipe views for switching between tabs.

You should first follow Create a first Android application tutorial, if this is your first time to create an Android project

Test your project build

Run and test your project. Get feeling how this project template works by default. You can click tabs to show different fragments and the same happens, when you swipe fragments left and right. In the other words, the working behaviour is the same.

Add a Volley to load JSON and Glide to load images

Open the build.gradle file for your app module and add a Volley and Glide libraries to the dependencies section. Remember always check the newest version numbers from Android developer site and Glide web site.

implementation 'com.android.volley:volley:1.1.1'
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'

Allow internet permission in manifest

Remember add internet permission to project manifest file. Otherwise your application can’t load data from the internet.

<uses-permission android:name="android.permission.INTERNET"/>

Remove tabs

This time only swipe will be used in the UI, so remove tab codes from the following files:

  • the string resources – strings.xml
  • MainActivity’s TableLayout – activity_main.xml
  • SectionsPagesAdapter codes TAB_TITLES, getPageTitle() – SectionsPagesAdapter.kt
  • Tabs code in onCreate() function – MainActivity.kt
  • Remove textView which uses section_label – PlaceholderFragment.kt

Constructor for the FragmentPagerAdapter is deprecated, so you might need to modify your class declaration as below. It will depends which version of Android Studio you are using.

class SectionsPagerAdapter(private val context: Context, fm: FragmentManager) 
  : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

Test your project build

Run and test your project. It should work now without tabs.

Forecast class

Create a new Forecast class, which will hold the forecast data used in this application. Only city, condition, temperature, time and icon will be used in this tutorial. However, you can use as much as data from the loaded forecast as you want.

class Forecast(
  val city: String,
  val condition: String,
  val temperature: String,
  val time: String,
  val icon: String
)

Layout for the weather forecast

Modify fragment_main.xml layout to display weather forecast information. Use your own imagination to layout UI elements. However, name your elements: cityTextView, conditionTextView, temperatureTextViewtimeTextView and iconImageView.

Weather Forecast JSON data includes a lot of different weather data, use them as you want!

Add one ProgressBar widget to the center of the UI in activity_main.xml and give id progressBar for it. This progressBar will be shown, when the JSON data is loading.

Load weather forecast JSON with Volley

API Key

Add your API key and web links to OpenWeather web site to MainActivity class. They will be used, when a data is loaded with Volley. Add a few example cities to MutableList. In the other words, the hardcoded city names will be used in this tutorial. Of course you can modify this tutorial; ask city name and save it to the device.

class MainActivity : AppCompatActivity() {
  // example call is : https://api.openweathermap.org/data/2.5/weather?q=Jyväskylä&APPID=YOUR_API_KEY&units=metric&lang=fi
  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"

  // add a few test cities
  val cities: MutableList<String> = mutableListOf("Jyväskylä", "Helsinki", "Oulu", "New York", "Tokyo")
  // city index, used when data will be loaded
  var index: Int = 0

Store loaded data to MutableList

Add a forecasts MutableList to MainActivity. This object will hold all the loaded weather forecast data. This data will be used in many classes in the application, so it is good to use inside a Companion Object.

companion object {
  var forecasts: MutableList<Forecast> = mutableListOf()
}

Start loading weather forecast data

Create own loadWeatherForecast function, which will be called, when the application is launched from the Android Studio. In other words, loading will happened automatically after app is launched.

// load forecast
private fun loadWeatherForecast(city:String) {
  // url for loading
  val url = "$API_LINK$city&APPID=$API_KEY&units=metric&lang=fi"

  // 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("WEATHER", "***** error: $e")
        // hide progress bar
        progressBar.visibility = View.INVISIBLE
        // show Toast -> should be done better!!!
        Toast.makeText(this,"Error loading weather forecast!",Toast.LENGTH_LONG).show()
      }
    },
    Response.ErrorListener { error -< Log.d("PTM", "Error: $error") })
  // start loading data with Volley
  val queue = Volley.newRequestQueue(applicationContext)
  queue.add(jsonObjectRequest)
}

Volley will call Response.Listener<JSONObject> with JSON response data, when a loading has been finished successfully. After that, you will need to parse the loaded JSON data and create Forecast object (that will be created later in this tutorial).

You should be familiar with the returned JSON data from the OpenWeather. Example data for Jyväskylä is visible below. Highlighted lines will be used in this tutorial.

{
   "coord":{
      "lon":25.75,
      "lat":62.24
   },
   "weather":[
      {
         "id":804,
         "main":"Clouds",
         "description":"overcast clouds",
         "icon":"04d"
      }
   ],
   "base":"stations",
   "main":{
      "temp":0.15,
      "feels_like":-4.46,
      "temp_min":0,
      "temp_max":0.56,
      "pressure":1009,
      "humidity":94
   },
   "visibility":6000,
   "wind":{
      "speed":3.6,
      "deg":230
   },
   "clouds":{
      "all":90
   },
   "dt":1578312865,
   "sys":{
      "type":1,
      "id":1337,
      "country":"FI",
      "sunrise":1578296324,
      "sunset":1578315956
   },
   "timezone":7200,
   "id":655195,
   "name":"Jyväskylä",
   "cod":200
}

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)

After that, 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()
// icon
val weatherIcon = firstWeatherObject.getString("icon")
val url = "$API_ICON$weatherIcon.png"

After that, create Forecast object with the loaded data and add it to the forecasts list. Use a Log-messages to debug your application. Start loading a next weather forecast, if all of city forecasts is not loaded yet.

// add forecast object to the list
forecasts.add(Forecast(city,condition,temperature,dt,url))
// use Logcat window to check that loading really works
Log.d("WEATHER", "**** weatherCity = " + forecasts[index].city)
// load another city if not loaded yet
if ((++index) < cities.size) loadWeatherForecast(cities[index])
else {
    Log.d("WEATHER", "*** ALL LOADED!")
}

After that, call above function from the onCreate(). In other words, loading will start when the application is launched.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  // Load weather forecasts
  loadWeatherForecast(cities[index])

  ... 
}

Now application will show a Toast or an error message in the LogCat window, if there are error with the data loading. Remember use and show some nice “error” indicators in the UI in real world applications!

Test your project build

Run and test your project. Open LogCat window and you should see a few Log lines, which shows the loaded city names.

2020-01-06 13:46:12.693 9473-9473/fi.jamk.weatherapp D/WEATHER: **** weatherCity = Jyväskylä
2020-01-06 13:46:12.806 9473-9473/fi.jamk.weatherapp D/WEATHER: **** weatherCity = Helsinki
2020-01-06 13:46:12.880 9473-9473/fi.jamk.weatherapp D/WEATHER: **** weatherCity = Oulu
2020-01-06 13:46:12.947 9473-9473/fi.jamk.weatherapp D/WEATHER: **** weatherCity = New York
2020-01-06 13:46:13.015 9473-9473/fi.jamk.weatherapp D/WEATHER: **** weatherCity = Tokyo
2020-01-06 13:46:13.015 9473-9473/fi.jamk.weatherapp D/WEATHER: *** ALL LOADED!
2020-01-06 13:46:13.015 9473-9473/fi.jamk.weatherapp D/WEATHER: [fi.jamk.weatherapp.Forecast@fcf3adc, fi.jamk.weatherapp.Forecast@15347e5,...
}

If not, check your Android Studio’s LogCat window and try to figure out what is the problem. Sometimes you might need to uninstall app from the emulator/device to get data loading working correctly, if you have tested it multiple times before data loading or you have missed the internet permission from the manifest.

Remember remove or comment all of the Log lines or Toasts used for the debugging, before you are publishing application to the Google Play.

Setup UI

Create own setUI function and move all Adapter and Fab codes inside it from the onCreate function. This code will connect created SectionsPagerAdapter to ViewPager component. Disable progressBar visibility.

private fun setUI() {
  // hide progress bar
  progressBar.visibility = View.INVISIBLE
  // add adapber
  val sectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager)
  val viewPager: ViewPager = findViewById(R.id.view_pager)
  viewPager.adapter = sectionsPagerAdapter
  // add fab
  val fab: FloatingActionButton = findViewById(R.id.fab)
  fab.setOnClickListener { view ->
    Snackbar.make(view, "How you can add and save a new city?", Snackbar.LENGTH_LONG)
      .setAction("Action", null).show()
  }
}

Call above function, when all the data is loaded with the Volley. Now application should only show the progressBar component, when the weather forecasts are loading. Real UI with data will be shown, after loading is finished. Of course, UI is not ready yet. We will modify in the later.

Test your projecT

Run and test your project. You should see progress bar first and then a main layout.

PageViewModel

Modify PageViewModel to hold forecast object. ViewModel will hold the data inside a private variable. It can be modified with setForecast function and it can be returned with forecast.

class PageViewModel : ViewModel() {
    private val _forecast = MutableLiveData<Forecast>()

    val forecast: LiveData<Forecast> = Transformations.map(_forecast) {
        it
    }

    fun setForecast(forecast: Forecast) {
        _forecast.value = forecast
    }
}

In other words, now forecast object can be set and read from the view model.

SectionsPagerAdapter

Modify SectionsPagerAdapter to pass position correctly to new instance creation and getCount to return the size of the forecast list.

override fun getItem(position: Int): Fragment {
  // A new placeholder fragment
  return PlaceholderFragment.newInstance(position)
}

override fun getCount(): Int {
  // Size of the forecast in MainActivity
  return MainActivity.forecasts.size
}

PlaceholderFragment

Modify PlaceholderFragment to create a view and model from the forecast object. In other words, fragment needs to know the position of the city in the forecasts array and view model need to pass correct forecast to UI layout.

First set the created companion object work with forecast data and more specially with position in the MutableList.

companion object {
  private const val ARG_FORECAST_POSITION = "forecast_position"

  @JvmStatic
  fun newInstance(position: Int): PlaceholderFragment {
    return PlaceholderFragment().apply {
      arguments = Bundle().apply {
        putInt(ARG_FORECAST_POSITION, position)
      }
    }
  }
}

After that, modify onCreatefunction to connect forecast object from the correct position to live data.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  pageViewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
    val position: Int = arguments!!.getInt(ARG_FORECAST_POSITION)
    setForecast(MainActivity.forecasts[position])
  }
}

Finally, modify onCreateView to show a weather forecast in the UI.

override fun onCreateView(
  inflater: LayoutInflater, container: ViewGroup?,
  savedInstanceState: Bundle?
): View? {
  val root = inflater.inflate(R.layout.fragment_main, container, false)
  pageViewModel.forecast.observe(this, Observer<Forecast> {
    cityTextView.text = it.city
    conditionTextView.text = it.condition
    temperatureTextView.text = it.temperature
    timeTextView.text = it.time
    Glide.with(this).load(it.icon).into(iconImageView)
  })

  return root
}

Test

You Application should work now – test try to swipe left and right.

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