Photo by Wolfgang Hasselmann on Unsplash

This article describes how to develop a real-time weather prediction app in Flutter using the Tomorrow.io Weather API. The app consists of a single screen that displays the current weather and a 4-day daily forecast for a specific location. We’re creating this prediction app on Flutter because it is an open-source UI development kit, which means it can be integrated beautifully into desktop and mobile apps across various platforms, making it scalable. 

Prerequisites

If you wish to follow along with this tutorial, you must have the following set up:

  • A free Tomorrow.io account to access their weather-related features and data

  • Any IDE that has the Flutter SDK installed (i.e., Android Studio, VSCode)

  • A basic understanding of Dart and Flutter

This tutorial was verified with Flutter v2.5.1 and Android Studio v3.5. 

So, with everything out the way, let's get started.

Getting started with Tomorrow.io

Tomorrow.io is a SaaS weather intelligence platform that provides real-time weather forecasts. It offers one of the most reliable free-to-use weather APIs. They provide an all-in-one endpoint that makes it easy to retrieve weather data by making a single call to the endpoint.

After creating an account with Tomorrow.io, we need to retrieve our apikey from the Development menu on the sidebar.  The image below shows the Development screen. 

Project Setup

Step 1: Creating a new Flutter project

Create a new Flutter project by running the following command in your terminal.

flutter create weather_app

Step 2: Installing Dependencies 

Next, we need to add the dio package as a dependency in our project. The dio package will be used for making network requests. 

Run the following command to get the newest version of the dio package in your project.

flutter pub add dio

This command automatically adds the dio package in your pubspec.yaml file.

Step 3 : Creating the Weather Model

Let's create a weather model class. The class should have instance variables, a constructor for initializing fields upon object creation, and methods for translating JSON data from the API to Dart Classes. It is recommended that we write a model class to make our code easier to read, understand, and maintain.

Create a new dart file weather_model.dart and paste the following code in it.  

import 'dart:convert';

Weather weatherFromJson(String str) => Weather.fromJson(json.decode(str));

String weatherToJson(Weather data) => json.encode(data.toJson());

class Weather {

  Weather({

    required this.data,

    required this.warnings,

  });

  Data data;

  List<Warning> warnings;

  factory Weather.fromJson(Map<String, dynamic> json) => Weather(

        data: Data.fromJson(json["data"]),

        warnings: List<Warning>.from(

            json["warnings"].map((x) => Warning.fromJson(x))),

      );

  Map<String, dynamic> toJson() => {

        "data": data.toJson(),

        "warnings": List<dynamic>.from(warnings.map((x) => x.toJson())),

      };

}

class Data {

  Data({

    required this.timelines,

  });

  List<Timeline> timelines;

  factory Data.fromJson(Map<String, dynamic> json) => Data(

        timelines: List<Timeline>.from(

            json["timelines"].map((x) => Timeline.fromJson(x))),

      );

  Map<String, dynamic> toJson() => {

        "timelines": List<dynamic>.from(timelines.map((x) => x.toJson())),

      };

}

class Timeline {

  Timeline({

    required this.timestep,

    required this.startTime,

    required this.endTime,

    required this.intervals,

  });

  String timestep;

  DateTime startTime;

  DateTime endTime;

  List<Interval> intervals;

  factory Timeline.fromJson(Map<String, dynamic> json) => Timeline(

        timestep: json["timestep"],

        startTime: DateTime.parse(json["startTime"]),

        endTime: DateTime.parse(json["endTime"]),

        intervals: List<Interval>.from(

            json["intervals"].map((x) => Interval.fromJson(x))),

      );

  Map<String, dynamic> toJson() => {

        "timestep": timestep,

        "startTime": startTime.toIso8601String(),

        "endTime": endTime.toIso8601String(),

        "intervals": List<dynamic>.from(intervals.map((x) => x.toJson())),

      };

}

class Interval {

  Interval({

    required this.startTime,

    required this.values,

  });

  DateTime startTime;

  Values values;

  factory Interval.fromJson(Map<String, dynamic> json) => Interval(

        startTime: DateTime.parse(json["startTime"]),

        values: Values.fromJson(json["values"]),

      );

  Map<String, dynamic> toJson() => {

        "startTime": startTime.toIso8601String(),

        "values": values.toJson(),

      };

}

class Values {

  Values({

    required this.windSpeed,

    required this.windDirection,

    required this.temperature,

    required this.temperatureApparent,

    required this.weatherCode,

    required this.humidity,

    required this.visibility,

    required this.dewPoint,

    required this.precipitationType,

    required this.cloudCover,

  });

  double windSpeed;

  double windDirection;

  double temperature;

  double temperatureApparent;

  int weatherCode;

  double humidity;

  double visibility;

  double dewPoint;

  int precipitationType;

  double cloudCover;

  factory Values.fromJson(Map<String, dynamic> json) => Values(

        windSpeed: json["windSpeed"].toDouble(),

        windDirection: json["windDirection"].toDouble(),

        temperature: json["temperature"].toDouble(),

        temperatureApparent: json["temperatureApparent"].toDouble(),

        weatherCode: json["weatherCode"],

        humidity: json["humidity"].toDouble(),

        visibility: json["visibility"].toDouble(),

        dewPoint: json["dewPoint"].toDouble(),

        precipitationType: json["precipitationType"],

        cloudCover: json["cloudCover"].toDouble(),

      );

  Map<String, dynamic> toJson() => {

        "windSpeed": windSpeed,

        "windDirection": windDirection,

        "temperature": temperature,

        "temperatureApparent": temperatureApparent,

        "weatherCode": weatherCode,

        "humidity": humidity,

        "visibility": visibility,

        "dewPoint": dewPoint,

        "precipitationType": precipitationType,

        "cloudCover": cloudCover,

      };

}

class Warning {

  Warning({

    required this.code,

    required this.type,

    required this.message,

  });

  int code;

  String type;

  String message;

  factory Warning.fromJson(Map<String, dynamic> json) => Warning(

        code: json["code"],

        type: json["type"],

        message: json["message"],

      );

  Map<String, dynamic> toJson() => {

        "code": code,

        "type": type,

        "message": message,

      };

}

The weather model is based on the structure of the JSON data we are to receive from the endpoint.

Step 4: Setting up the ApiClient

After creating the weather model class, we'll need to construct our ApiClient class, which will handle network calls to the API endpoint.

The code snippet below shows how we get the weather data of a specific location using the Dio package. First, we initialize the Dio package with our baseUrl. Then, we specify our query parameters apikey, location, fields, units, timesteps, startTime, and endTime.  

You can access the Tomorrow.io documentation to see a preview of the query parameters accepted by the Tomorrow API, and what data you can get from the fields attribute.

Next, we create a method called getWeather, inside which we call the GET method on the Dio object and pass in the required parameters. Then, we await the response. If successful, we parse the JSON data and return it; otherwise, we produce an error message.

import 'package:dio/dio.dart';

import 'package:tomorrow_weather/model/weather.dart';

class ApiClient {

  final Dio _dio = Dio(BaseOptions(

    baseUrl: "https://api.tomorrow.io/v4",

  ));

  //replace with your ApiKEY

  //get your key from app.tomorrow.io/development/keys

  static const String apiKey = 'YOUR_API_KEY';

  //pick a location to get the weather

  static const location = [40.758, -73.9855]; //New York

  //// list the fields you want to get from the api

  static const fields = [

    "windSpeed",

    "windDirection",

    "temperature",

    "temperatureApparent",

    "weatherCode",

    "humidity",

    "visibility",

    "dewPoint",

    "cloudCover",

    "precipitationType"

  ];

  // choose the unit system, either metric or imperial

  static const units = "imperial";

  // set the timesteps, like "current" and "1d"

  static const timesteps = ["current", "1d"];

  // configure the time frame up to view the current and 4-days weather forecast

  String startTime =

      DateTime.now().toUtc().add(const Duration(minutes: 0)).toIso8601String();

  String endTime =

      DateTime.now().toUtc().add(const Duration(days: 4)).toIso8601String();

  //method to get the weather data

  Future<Weather> getWeather() async {

    try {

      final response = await _dio.get('/timelines', queryParameters: {

        'location': location.join(','),

        'apikey': apiKey,

        'fields': fields,

        'units': units,

        'timesteps': timesteps,

        'startTime': startTime,

        'endTime': endTime

      });

      //parse the JSON data, returns the Weather data 

      return Weather.fromJson(response.data);

    } on DioError catch (e) {

      //returns the error if any

      return e.response!.data;

    }

  }

}

That concludes the ApiClient class. Next, we'll build the UI for our weather application and consume the API data.

Step 5: Consuming the API Data

We have a single screen that displays the current and 4-day forecast weather statistics. 

The code snippet below shows how we used the FutureBuilder weather widget to display the result of our getWeather method. If it contains data, it returns the weather data; else, it renders an error on the screen. If none of the aforementioned apply, we display the CircularProgressIndicator widget to indicate that our data is loading.

//...

//...

class _HomePageState extends State<HomePage> {

  final ApiClient _apiClient = ApiClient(); //initialize the ApiClient 

  

  Widget build(BuildContext context) {

    return Scaffold(

      backgroundColor: AppColors.backgroundColor,

      body: SafeArea(

        child: FutureBuilder<Weather>(     //<<-- FutureBuilder 

            future: _apiClient.getWeather(),

            builder: (context, snapshot) {

              //loading state

              if (snapshot.connectionState == ConnectionState.waiting) {

                return const Center(

                  child: CircularProgressIndicator(),

                );

              } else {

                //when the future is complete and there is an error, display it.

                if (snapshot.hasError) {

                  return Center(

                    child: Text(

                      'Error: ${snapshot.error}',

                      style: const TextStyle(color: Colors.red),

                    ),

                  );

                } else {

                  //when the future is complete and has no error, show the weather.

                  //get weatherCode

                  int weatherCode = snapshot

                      .data!.data.timelines[0].intervals[0].values.weatherCode;

                  //get weatherName

                  String weatherCodeName =

                      ApiClient.getWeatherCodeName(weatherCode);

                  //get weatherIcon

                  String weatherCodeIcon =

                      ApiClient.getWeatherIcon(weatherCodeName);

                  return Stack(

                    children: [

                      //...

                      Padding(

                        padding: const EdgeInsets.symmetric(

                          vertical: 15,

                          horizontal: 15.0,

                        ),

                        child: ListView(

                          shrinkWrap: true,

                          children: [

                            const Text('Current Weather',

                                style: TextStyle(

                                  color: AppColors.greyShade1,

                                  fontSize: 30.0,

                                  fontWeight: FontWeight.w700,

                                )),

                            const SizedBox(height: 30.0),

                            Image.asset(

                              weatherCodeIcon,   //weatherIcon

                              width: 150,

                              height: 150,

                            ),

                            Center(

                              child: RichText(

                                text: TextSpan(

                                  //accessing the temperature value from the snapshot

                                  text: snapshot.data!.data.timelines[0]

                                      .intervals[0].values.temperature

                                      .toStringAsFixed(0)

                                      .toString(),

                                  style: const TextStyle(

                                      fontFamily: 'Raleway',

                                      fontSize: 144,

                                      color: AppColors.greyShade1,

                                      fontWeight: FontWeight.w500),

                                  children: const [

                                    TextSpan(

                                      text: '°F',

                                      style: TextStyle(

                                        fontFamily: 'Raleway',

                                        fontSize: 48,

                                        color: AppColors.textBlueColor,

                                        fontWeight: FontWeight.w500,

                                      ),

                                    )

                                  ],

                                ),

                              ),

                            ),

//...

//...

Conclusion

In this tutorial, we've learned what Tomorrow.io is and how we can interact with its API to build a weather prediction application. The source code of the demo application is available on GitHub. We’ve done this using Flutter to enable integration into desktop and mobile applications, making it scalable for any use – private or public.