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!
Nice article. Love the way you layered it. Might I suggest adding an exception handler though? I got an error running it under Windows 10.
procedure TfrmMain.Start;
begin
{$IF Defined(DEBUG) and Defined(MSWINDOWS)}
FAPI.GetByCoords(-34.8877, 138.5833); // Dave's suburb in Adelaide, Australia!
{$ENDIF}
try
LocationSensor.Active := True;
except
//the location sensor is not available so just show the default location but let the user know what is happening
on E: EOleSysError do
begin
MessageDlg(Format('An ''%s'' error was reported. The Location Sensor is off or unavailable. Please check your device settings.',[E.Message]),TMsgDlgType.mtError,[TMsgDlgBtn.mbOk],0);
end;
end;
end;
Yes, my bad.. That’s what happens when you commit code that you “clean up” and not test 😉 I’ll take care of it in the next day or so
It works on my Windows 10 VM. I guess my Macbook’s sensor is being virtualized? 🙂
Thanks for the heads up, anyway
Hi, Great stuff!
your next trick is to figure out how to actually submit formated weather data to open weather map…something I have not figured out myself 🙂
Brian
One thing you might want to address is why a developer should opt for Delphi for mobile development (large cost) vs. VisualStudio with Xamarin (free). Even if VS.NET currently has some rough edges you know M$ will iron them out, and it’s not like Delphi hasn’t had it’s issues with FMX or new releases of Android and iOS. C# has a much larger community than Delphi.
good thing that there has not been any breaking changes to android or iOS for a while now
Larry,
If I’m correct Xamarin doesn’t produce native apps (down to the processor). It’s managed, like C#, .NET, Java. That means it’s recompiled or interpreted again and thus, should you really debug it down to the CPU, it’s not your code anymore.
Native means also way better speeds.
On mobile, Xamarin produces native code on iOS, managed code on Android and Windows, however it has an option for AOT (Ahead Of Time) for Windows. I’m yet to dive headlong into Xamarin; I do plan to, to at least familiarise myself more with it. I am curious as to how it performs on Android.
I get “access is denied” on windows
That’s likely to be due to an invalid API key. Did you apply for one? The code will not compile unless you provide one.
yes of course I got new api key also I tested on android I dont get any readings at all
on Android I dont get any readings at all.
Thank you for mentioning the weather service I had been developing. Unfortunately, this became a dead project as I have no sure ways to monetize it. Nobody would want to pay both myself AND the underlying weather service, and since it’s an API, I also can’t really put ads in front of users either (besides the website which hosts documentation, etc.). I also no longer own that domain name, I let it expire not long ago.
That being said, it was actually a two part project. The main part was a Delphi component with a thread which automagically fetched weather data and triggered events when they were received. The other project was the Web API which could allow you to merge data from multiple weather services, and average all the numbers together. The first part, I may continue. However, the second part I will not, because I would need a dedicated web server, which I am not willing to pay for unless I can monetize it (which is next to impossible, considering the nature).
So, I would recommend changing your link above to the GitHub repository:
https://github.com/djjd47130/Weather
Download free trial from http://fmxlinux.com/ and add Linux support. 🙂
Hi, very nice article
how to use weather map with delphi ?