Developing software with Delphi is easy regardless of “cross-platformness”, relatively speaking, in comparison to other tools I’ve used, however this article shows how insanely easy it can be to put together an application that runs on iOS, Android, Windows and MacOS.

UPDATE: Developer Jerry Dodge has alerted me to this project of his, called JD Weather, that supports a whole bunch of weather services using the same API! You should be able to easily modify the demo here to use it.

Just over a week ago (in May, 2017), Jim McKeeth and I were talking about Xamarin, and how it can be a challenge to develop cross-platform apps with it. He was referring specifically to this article from Tim Anderson, how a programming challenge from Paul Thurrott showed a few bumps in Microsoft’s path to cross-platform mobile. (The challenge has since been concluded).

Jim asked me if I’d be interested in writing an article about how easy it can be to develop a cross-platform weather app. There’s already a plethora of apps out there; this was just an exercise it how easily it can be done with Delphi.

Jim supplied me with a link to an API, namely this one from OpenWeatherMap. He also pointed me to some free clipart on Pixabay, however apart from that, I was all on my own.

The result is the demo app that you can download from here. It showcases some of the technologies available in Delphi that you can use to easily build a cross-platform weather app, namely:

  • FMX controls
  • Json Parsing support from the REST.Json unit
  • Cross-platform HTTP communications using THTTPClient
  • Location Services, using TLocationSensor

The first task was to examine the API, and see what I’d need to do in order to communicate with it. In order to use any of the OpenWeatherMap APIs, you need an API key. Fortunately, they have one that you can use for free (with usage limitations), and you can apply for one here.

The OpenWeatherMap APIs have different payload types, the default of which is JSON; the other two are XML and HTML. I chose JSON, because I knew I could quickly build some classes that I could easily populate using parsing from the REST.Json unit.

The JSON version of the API call produces a payload like this:

[sourcecode language=”Javascript”] {
"coord":
{
"lon": 138.58,
"lat": -34.89
},
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clearsky",
"icon": "01d"
}
],
"base": "stations",
"main":
{
"temp": 292.15,
"pressure": 1020,
"humidity": 42,
"temp_min": 292.15,
"temp_max": 292.15
},
"visibility": 10000,
"wind":
{
"speed": 5.7,
"deg": 240
},
"clouds":
{
"all": 0
},
"dt": 1494738000,
"sys":
{
"type": 1,
"id": 8204,
"message": 0.0044,
"country": "AU",
"sunrise": 1494711125,
"sunset": 1494748298
},
"id": 2062944,
"name": "Prospect",
"cod": 200
}[/sourcecode]

Instead of just sticking all the code in the main form unit (which might be the case for much simpler demos), I decided I’d split up the functionality into distinct parts:

  • OW.Data contains the classes used for receiving the JSON data
  • OW.Consts contains the constants used by the OW.API unit
  • OW.API has the TOpenWeatherAPI class that does the work of making the API call, and returning the data

You’ll notice I named the class that represents the JSON data: TOpenWeatherByCoords. I named it this way in case data structures for other types of calls are different. I’ve left it as an exercise for the reader to determine whether they are. You may also note that the TOpenWeatherRain class has the private field exposed as public in a property called: vol3h. This is because it’s not possible to have an identifier in Delphi that starts with a number (it would otherwise been 3h).

The code for the TOpenWeatherAPI class is fairly simple. The GetByCoords method simply formats the URL, and fires off a task:

[sourcecode language=”Delphi”] procedure TOpenWeatherAPI.GetByCoords(const ALatitude, ALongitude: Double);
var
LQuery: string;
begin
LQuery := Format(cOpenWeatherByCoordsQuery, [cOpenWeatherAPIKey, ALatitude, ALongitude]);
TTask.Run(
procedure
begin
DoSendRequest(cOpenWeatherByCoordsURL + LQuery, TOpenWeatherRequest.ByCoords);
end
);
end;[/sourcecode]

that creates a THTTPClient, calls Get on the URL:

[sourcecode language=”Delphi”] procedure TOpenWeatherAPI.DoSendRequest(const AURL: string; const ARequest: TOpenWeatherRequest);
var
LHTTP: THTTPClient;
LResponse: IHTTPResponse;
begin
LHTTP := THTTPClient.Create;
try
LResponse := LHTTP.Get(AURL);
if LResponse.StatusCode = cHTTPResultOK then
DoProcessContent(LResponse.ContentAsString, ARequest)
else ; // Left as an exercise to the reader
finally
LHTTP.Free;
end;
end;[/sourcecode]

and processes the result. DoProcessContentByCoords uses TJson to parse the result and send the resulting JSON object to the OnByCoords event, of course synchronised with the main thread:

[sourcecode language=”Delphi”] procedure TOpenWeatherAPI.DoProcessContentByCoords(const AContent: string);
var
LByCoords: TOpenWeatherByCoords;
begin
try
LByCoords := TJson.JsonToObject<TOpenWeatherByCoords>(AContent);
TThread.Synchronize(nil,
procedure
begin
DoByCoords(LByCoords);
end
);
except
// Left as an exercise to the reader
end;
end;[/sourcecode]

In the main form, I have an instance of TOpenWeatherAPI and a handler for OnByCoords, called APIByCoordsHandler. In that method is the code for taking the data from the JSON object and populating the controls on the form:

[sourcecode language=”Delphi”] procedure TfrmMain.APIByCoordsHandler(Sender: TObject; const AByCoords: TOpenWeatherByCoords);
var
LWeather: TOpenWeatherWeatherItem;
begin
LocationLabel.Text := AByCoords.name;
TemperatureLabel.Text := GetTemperatureText(AByCoords.main.temp);
if Length(AByCoords.weather) > 0 then
begin
LWeather := AByCoords.weather[0];
WeatherImage.Bitmap.LoadFromURL(cOpenWeatherWeatherImagesURL + LWeather.icon + ‘.png’);
WeatherLargeImage.Bitmap.LoadFromFile(TPath.Combine(FImagesPath, LWeather.icon.Substring(0, 2) + ‘.png’));
WeatherMainLabel.Text := LWeather.main;
end;
if AByCoords.rain <> nil then
RainValueLabel.Text := Format(‘%.1f mm’, [AByCoords.rain.vol3h])
else
RainValueLabel.Text := ‘Nil’;
HumidityValueLabel.Text := Format(‘%.0f %’, [AByCoords.main.humidity]);
PressureValueLabel.Text := Format(‘%.1f hPa’, [AByCoords.main.pressure]);
WindSpeedValueLabel.Text := Format(‘%.1f km/h’, [AByCoords.wind.speed]);
WindDirectionValueLabel.Text := BearingToDirection(AByCoords.wind.degrees);
end;[/sourcecode]

You may notice a method on the Bitmap called LoadFromURL, which loads the image corresponding to the icon information passed in the JSON object. Where did LoadFromURL come from? The answer is in the OW.Graphics.Net.Helpers unit: I created a helper class that adds the LoadFromURL method to TBitmap, which uses a similar technique as the GetByCoords method of the TOpenWeatherAPI class. In this case, it sends off an asynchronous call to get the image from a URL, then synchronously calls LoadFromStream to load the image into the bitmap.

Back in the main form, the whole process is kicked off when the TLocationSensor receives a location:

[sourcecode language=”Delphi”] procedure TfrmMain.LocationSensorLocationChanged(Sender: TObject; const OldLocation, NewLocation: TLocationCoord2D);
begin
// Have location now, so turn off the sensor
LocationSensor.Active := False;
FAPI.GetByCoords(NewLocation.Latitude, NewLocation.Longitude);
end;[/sourcecode]

If you want the app to update when the user changes location, you’ll need to make sure the sensor is always on, however be aware of the usage limitations with the free API key.

Here’s a screenshot of the app running on Android:

It looks very similar on iOS, Windows and OSX, so I won’t bother putting up images of those; you can try it out for yourself! Remember that you if you use the OpenWeatherMap API, you will need to apply for an API key, which is free for limited use. Just replace the value for the cOpenWeatherAPIKey constant in OW.Consts with your API key value.

Some other exercises for the reader:

  • Show readings in units as chosen by the user, or by the users location, e.g. Temperature (conversion functions are supplied for this), rain volume, etc
  • Allow users to change the location without reference to the TLocationSensor
  • Use another API, or use more OpenWeatherMap APIs to retrieve more information
  • Make the UI a little better, including stretching of the background image

The trick here is that I built this project in only a few hours, and good part of that time was spent fiddling around with the UI (which I’m still not happy with). The best part is that from this one, single source project (take that, Xamarin!), I am able to deploy to iOS, Android, Windows and MacOS.

Enjoy!