How to create a console application in dart using the weather_pack package?

Ruble
12 min readJan 31, 2023

--

In this article, we will look at how to create a console application that allows you to get weather data for a selected location and display them in the console, using the dart programming language and the weather_pack library. We will also learn how to use the location geocoding service and consider convenient classes that allow us to convert values into the desired units of measurement.

The weather_pack package has been developed to simplify access to the online service https://openweathermap.org/ (hereinafter, OWM), and to provide convenient methods for working with the received data. The following manual will stick to the official recommendations for use (readme.md project), but with some bonuses.

Let’s get started!

Table of Contents

Foreword

This material was created to solve a number of following problems:

  • get acquainted with the Medium platform as a content creator (I have already noticed the absence of the dart language in the code block. Medium, please add support for the programming language Dart. I have chosen JavaScript as the most successful syntax highlighting language (eh, kindred spirits :> ) )
  • tell how to create a console application using the dart programming language
  • consider using the weather_pack package

The weather_pack library is independent of the flutter framework and can be used, for example, in console applications of dart. That's how, by creating a console application, we will test this package.

Preparatory phase

First, let’s create our dart application. Navigate to the desired folder and run the command:

dart create --template=console weather_in_console

The --template=console flag creates a simple console template application.

Now we need to add the package as a dependency in our pubspec.yaml file. Let's use the command:

dart pub add weather_pack

Or, let’s add manually:

dependencies:

# from pub.dev
weather_pack: ^<latest_actual_version>

# or, if the package was downloaded and is located in the local folder
weather_pack:
path: ./path_to_package/weather_pack

# or, straight from github
weather_pack:
git:
url: https://github.com/PackRuble/weather_pack.git
ref: master

Then run the command dart pub get. After all these manipulations we have this folder structure.

The preparatory phase is complete, we can start using the package.

Getting the first weather data

There are currently two main weather classes in version 0.0.2: WeatherCurrent and WeatherOneCall. The second model is more complex and includes WeatherCurrent:

  1. WeatherCurrent - characteristics of the current weather
  2. List<WeatherHourly> - a list of weather patterns that differ by an hour. Includes about 48 objects with different parameters.
  3. List<WeatherMinutely> - a list of models characterized by a minute difference and as a field having only precipitation - the amount of precipitation at a given time.
  4. List<WeatherDaily> - a list of 7 objects, which includes an extensive amount of parameters characterizing each subsequent day
  5. List<WeatherAlert> - weather alerts. Fields: the start date and the end date of the event, the event itself and its description. There are also the sender's name and tags.

Two basic types of models means that these are different requests to the server. And the query to get WeatherOneCall is more expensive (was so: 1000/day and 30,000/month). At the moment, I cannot find this information for the number of requests specifically for One Call API 2.5 (previously this api was called "one call api 2.0") and did not include the 5-day historical weather. Moreover, the above link previously took us to version 2.0, on the api of which this package is based).

And now there is a tariff “one call api 3.0”, which offers 1000 free requests per day, but if you exceed the limit — pay. And in general, before you can use it, you need to sign up for this rate by filling out the form below and … “Continue with Stripe” as if to hint that it’s going to be fun. In short, business and marketing, nothing personal.

Therefore, our basic queries will look like this:

/// Get weather [WeatherCurrent].
Future<WeatherCurrent> _getWeatherCurrent(PlaceGeocode city) async {
return _wService.currentWeatherByLocation(
latitude: city.latitude ?? 0.0,
longitude: city.longitude ?? 0.0,
);
}

/// Get weather [WeatherOneCall].
Future<WeatherOneCall> _getWeatherOneCall(PlaceGeocode city) async {
return _wService.oneCallWeatherByLocation(
latitude: city.latitude ?? 0.0,
longitude: city.longitude ?? 0.0,
);
}

I want to draw your attention that the service OWM recommends to use the weather search exactly by coordinates. Previously it was possible using the city, however:

Please use Geocoder API if you need automatic convert city names and zip-codes to geo coordinates and the other way around.

Please note that built-in geocoder has been deprecated. Although it is still available for use, bug fixing and updates are no longer available for this functionality.

Therefore, the package has a class GeocodingService which provides two methods: getting a list of locations by coordinates (getLocationByCoordinates()) and by the supposed name of the location (getLocationByCityName()).

It’s a good time to go deeper into the architecture of our mini-application.

Architecture

As such, it will be absent. The input point of the application is the file in the path bin\main.dart. The code in it is as follows:

import 'package:weather_in_console/weather_in_console.dart' as service_owm;

Future<void> main(List<String> arguments) async {
await service_owm.getWeather();
}

All of the logic code will (and should) be contained in the file under lib/weather_in_console.dart . Let's make everything private with _ and define one single public function getWeather().

Our application will work according to the flowchart:

Created by Mermaid

Writing the code

We will write the code step by step, according to the above scheme. All actions take place in the file lib/weather_in_console.dart.

First, we define our only public function:

import 'dart:io';
import 'package:weather_pack/weather_pack.dart';

// global scope

/// The weather function
Future<void> getWeather() async {
stdout.writeln(' ✨ Welcome to the weather service! ✨ \n');

// local scope of getWeather() function

}

In the local scope of the getWeather() function all the basic manipulations and calls to other, non-public functions will take place. Let us call this area local.

First, you need to check the api key for relevance. Let’s simulate this section here:

In the global scope we define a private function:

Future<void> _checkApiKey(String apiKey) async {
final bool isCorrectApiKey = await OWMApiTest().isCorrectApiKey(_apiKey);

if (isCorrectApiKey) {
stdout.writeln(' 🗝 The key is suitable for this lock ^_~');
} else {
stdout.writeln(' ❌ The api key does not fit!');
exit(1);
}
}

To check the key, use the OWMApiTest.isCorrectApiKey() batch method. If the key passes the test, continue executing the program, otherwise abort with exit(1). From the documentation:

Although you can use any number for an exit code, by convention, the codes in the table below have the following meanings:
0 — Success, 1 — Warnings, 2 — Errors

Now let’s call this function in the local area:

// If you do not use the `--define="API_WEATHER=YOUR_APIKEY"` flag,
// provide the key here instead of 'null'
const String _apiKey = null ?? String.fromEnvironment('API_WEATHER');

Future<void> getWeather() async {
...
await _checkApiKey(_apiKey);
}

A small digression. I want to draw your attention to the fact that I do not recommend storing secret keys and other private data in the code! In this case, you can specify your api key obtained from the OWM service (register on openweathermap.org and get the key in your personal cabinet on the API keys tab. This is free), instead of null, however, I recommend doing otherwise and using const String.fromEnvironment().

If you run the application in the terminal, use the special. flag:

dart run --define="API_WEATHER=YOUR_APIKEY" bin/main.dart

Alternatively, to use the handy Run tab, in Android Studio you can do this:

There is a good article on this subject here.

Next, the implementation of this part of the scheme:

would look like this:

import 'package:interact/interact.dart';

//...

/// Get the location where the user wants to know the weather.
String _inputCity() {
final input = Input(
prompt: 'Your location?',
).interact();

if (input.isEmpty) {
final isResume = Confirm(
prompt: 'The location is not specified. Shall I try again?',
defaultValue: true,
waitForNewLine: true, // we want the user to confirm the action with the enter key
).interact();

return isResume ? _inputCity() : exit(0);
} else {
return input;
}
}

Yes, in order to accept user input I decided to use an additional package to facilitate the command line. The package is called interact: ^2.1.1. Later on, it will come in handy for more complicated things.

https://pub.dev/packages/interact

After the user has specified the desired location, it is necessary to convert that location into coordinates. The built-in method GeocodingService.getLocationByCityName() from our weather_pack comes to the rescue.

In the global scope, define a private variable of the geocoding service, in the constructor of which you must specify the api key:

final _gService = GeocodingService(_apiKey);

and use this service to get a list of places that are similar to what we want:

Future<PlaceGeocode?> _selectCity(String desiredPlace) async {
final spinner = Spinner(
icon: ' 🏡 ',
rightPrompt: (done) => done
? 'You entered a location'
: 'We get a list of available locations...',
).interact();

final List<PlaceGeocode> places =
await _gService.getLocationByCityName(desiredPlace);

spinner.done();

if (places.isEmpty) {
stdout.writeln('But "$desiredPlace" not found <(_ _)>');
return null;
}

final index = Select(
prompt: 'Select a location from the list?',
options: places.map((PlaceGeocode place) {
final result = StringBuffer();

if (place.countryCode != null) {
result.write('${place.countryCode}: ');
}

if (place.state != null) {
result.write('${place.state}, ');
}

if (place.name != null) {
result.write('${place.name}');
}

return result.toString();
}).toList(),
).interact();

return places[index];
}

The Spinner and Select methods from the interact package

We use Spinner to make our interface friendlier and also to show that a request is now being made. As soon as the query is done, we terminate Spinner with the command spinner.done(). Next, return null if no similar places are found. Otherwise, suggest that the user select the most similar place from the list of places found. By the way, the getLocationByCityName() method takes the optional parameter limit to specify the maximum number of locations found. The default setting is a maximum of 5 seats (according to the api OWM).

In the local function, we’ll do the trick of repeating the whole process if the geocoding service returns an empty list (i.e. null):

Future<void> getWeather() async {

// ...

PlaceGeocode? city;
do {
final String desiredPlace = _inputCity();
city = await _selectCity(desiredPlace);
} while (city == null);

}

Well, once the desired location has been found and selected, let’s ask the user to select the type of weather data to be received. In the block diagram:

In code:

enum _TypeWeather {
hourly('Hourly weather'),
current('Current weather'),
daily('Weather for 7 days'),
alerts('Weather warnings'),
all('All data');

const _TypeWeather(this.name);

final String name;
}

/// Select the type of weather data.
_TypeWeather _selectTypeWeather() {
final index = Select(
prompt: 'What weather data to provide?',
options: _TypeWeather.values.map((e) => e.name).toList(),
initialIndex: 1,
).interact();

return _TypeWeather.values[index];
}

In local function:

Future<void> getWeather() async {

//...

final _TypeWeather typeWeather = _selectTypeWeather();
await _getWeather(city, typeWeather);
}

In the _getWeather() function we get the weather and display the results in the console. We also remember that the WeatherService.currentWeatherByLocation() rate is cheaper, so if we choose _TypeWeather.current, we make exactly the cheapest request (although, as we remember, the WeatherOneCall model includes WeatherCurrent).

/// Get the weather and print the results.
Future<void> _getWeather(PlaceGeocode city, _TypeWeather typeWeather) async {
final gift = Spinner(
icon: ' 🌤 ',
rightPrompt: (done) =>
done ? "Here's the reward for patience" : 'Getting weather data...',
).interact();

late WeatherCurrent current;
late WeatherOneCall oneCall;

if (typeWeather == _TypeWeather.current) {
current = await _getWeatherCurrent(city);
} else {
oneCall = await _getWeatherOneCall(city);
}

gift.done();

switch (typeWeather) {
case _TypeWeather.hourly:
_printHourly(oneCall.hourly);
break;
case _TypeWeather.current:
_printCurrent(current);
break;
case _TypeWeather.daily:
_printDaily(oneCall.daily);
break;
case _TypeWeather.alerts:
_printAlerts(oneCall.alerts);
break;
case _TypeWeather.all:
_printAlerts(oneCall.alerts);
_printHourly(oneCall.hourly);
_printDaily(oneCall.daily);
break;
}
}

final _wService = WeatherService(_apiKey, language: WeatherLanguage.russian);

/// Get weather [WeatherCurrent].
Future<WeatherCurrent> _getWeatherCurrent(PlaceGeocode city) async {
return _wService.currentWeatherByLocation(
latitude: city.latitude ?? 0.0,
longitude: city.longitude ?? 0.0,
);
}

/// Get weather [WeatherOneCall].
Future<WeatherOneCall> _getWeatherOneCall(PlaceGeocode city) async {
return _wService.oneCallWeatherByLocation(
latitude: city.latitude ?? 0.0,
longitude: city.longitude ?? 0.0,
);
}

The constructor WeatherService takes the optional parameter language to provide some weather data in the selected language, such as WeatherCurrent.weatherDescription, and PlaceGeocode.localNames - map, which contains location names in a specific language. The api currently provides 47 languages.

Functions such as _printDaily() display weather data in the console. The weather_pack package has convenient methods for converting and displaying weather data. Let's see how they work using _printCurrent() as an example:

void _printCurrent(WeatherCurrent current) {
final DateTime? date = current.date;

const pressureUnits = Pressure.mmHg;
const tempUnits = Temp.celsius;
const speedUnits = Speed.kph;

final temp = tempUnits.valueToString(current.temp!, 2);
final tempFeelsLike = tempUnits.valueToString(current.tempFeelsLike!, 2);

final windSpeed = speedUnits.valueToString(current.windSpeed!, 1);
final windGust = speedUnits.valueToString(current.windGust ?? 0.0, 2);

stdout.write('''
=====Today=====
${date?.day}.${date?.month}.${date?.year} ${date?.hour}h:${date?.minute}m
${current.weatherDescription}
Temperature $temp ${tempUnits.abbr}, Feels like $tempFeelsLike ${tempUnits.abbr}
Wind Speed - $windSpeed ${speedUnits.abbr}, gust to $windGust ${speedUnits.abbr}
Wind direction ${SideOfTheWorld.fromDegrees(current.windDegree!).name}
Pressure ( ${pressureUnits.valueToString(current.pressure!)} ${pressureUnits.abbr} )
Humidity ( ${current.humidity} % )
UV ( ${current.uvi} )
Sunrise and sunset ( ${current.sunrise} --> ${current.sunset} )
=================
''');
}

The entire list of available methods can be seen here.

Select the units of pressure, temperature, and velocity. Each enumeration has two conversion methods: value() and valueToString(). In short, the valueToString() method is needed to display the data correctly in the interface (to accurately control the number of decimal places), and the value() method is for accurate calculations. Each of these methods has a positional parameter int precision to specify the number of significant decimal places.

This code from “readme.md” describes well the essence of these methods:

void worksTempUnits({
double temp = 270.78, // ex. received from [WeatherCurrent.temp]
int precision = 3,
Temp unitsMeasure = Temp.celsius,
}) {
// The default temperature is measured in Kelvin of the `double` type.
// We need the temperature to be displayed in Celsius to 3 decimal places

print(unitsMeasure.value(temp, precision)); // `-2.37` type `double`
print(unitsMeasure.valueToString(temp, precision)); // `-2.370` type `String`

// if precision is 0:
print(unitsMeasure.value(temp, 0)); // `-2.0` type `double`
print(unitsMeasure.valueToString(temp, 0)); // `-2` type `String`
}

Another notable fact is that the wind direction from the api comes in degrees. With the SideOfTheWorld.fromDegrees() method we can translate the degrees into the direction of light.

There seems to be one last thing left in our flowchart:

After the transformations, our local function will look like this:

Future<void> getWeather() async {
stdout.writeln(' ✨ Welcome to the weather service! ✨ \n');

await _checkApiKey(_apiKey);

bool isRepeat = true;

do {
PlaceGeocode? city;
do {
final String desiredPlace = _inputCity();
city = await _selectCity(desiredPlace);
} while (city == null);

final _TypeWeather typeWeather = _selectTypeWeather();

await _getWeather(city, typeWeather);

isRepeat = _repeat();
} while (isRepeat == true);

exit(0);
}

bool _repeat() {
return Confirm(
prompt: 'Find out the weather of another location?',
defaultValue: true,
waitForNewLine: true,
).interact();
}

After displaying the data in the console, we will invite the user to find out the weather from another location. If he refuses, we end our application.

The flowchart seems to have come to an end.

Compile to .exe

Why not? It’s easy enough to do it with the following command:

dart compile exe bin/main.dart --define="API_WEATHER=YOUR_APIKEY"

And we get an .exe file that we can pass on to friends and acquaintances. With a caveat:

The exe subcommand has some known limitations:

No cross-compilation support (issue 28617)

The compiler can create machine code only for the operating system on which you’re compiling. To create executables for macOS, Windows, and Linux, you need to run the compiler three times. You can also use a continuous integration (CI) provider that supports all three operating systems.

See official documentation [here] (https://dart.dev/tools/dart-compile) for more details about available compilation features and useful flags (there is a cool optimization control flag).

Afterword

The result of the work in the gif:

Regardless of the language, the essence remains the same

There is a slight disappointment that in windows terminals (both cmd and powershell) I could not see the nice emoji I used in the code. However, through the newfangled terminal the problem is solved.

Also, I want to draw attention to the fact that we can not use the Cyrillic alphabet. It’s all about this problem, which still does not have an obvious solution.

Remark to the code base:

  1. Assumptions in the area of null checks (e.g. city.latitude ?? 0.0 or current.temp!) exist only because the author does not set a task to handle them correctly.
  2. There is no error handling at all, however, there is an OwmApiException class in weather_pack to help with some api errors. But, for example, TimeoutException is a matter for the user of the package.
  3. The absence of adequate architecture (we cannot call it adequate architecture to cram functions of different types into one file) of an application means only that all the code is in the example folder (and this is true).

Link to the repository of this project.

--

--