Home/Code tips, General tips, Uncategorized, Using APIs/Making cross-platform apps with Delphi is easy

Making cross-platform apps with Delphi is easy

Views:
641

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 site of his, called JD Weather, that supports a whole bunch of weather service 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:

{
  "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
}

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:

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;

that creates a THTTPClient, calls Get on the URL:

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;

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:

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;

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:

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;

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:

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;

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!

By | 2017-06-15T16:50:08+00:00 May 15, 2017 8:38 pm|Code tips, General tips, Uncategorized, Using APIs|12 Comments

About the Author:

12 Comments

  1. Larry Hengen May 16, 2017 at 3:23 am - Reply

    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;

    • admin May 16, 2017 at 6:10 am - Reply

      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

      • admin May 16, 2017 at 8:03 am - Reply

        It works on my Windows 10 VM. I guess my Macbook’s sensor is being virtualized? 🙂

        Thanks for the heads up, anyway

  2. Brian Hamilton May 16, 2017 at 4:51 am - Reply

    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

  3. Larry Hengen May 16, 2017 at 6:08 am - Reply

    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.

    • Brian Hamilton May 16, 2017 at 8:12 am - Reply

      good thing that there has not been any breaking changes to android or iOS for a while now

  4. Steve Jordi May 17, 2017 at 6:57 am - Reply

    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.

    • admin May 17, 2017 at 7:21 am - Reply

      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.

  5. Isaac Bekheit May 18, 2017 at 11:10 am - Reply

    I get “access is denied” on windows

    • admin May 19, 2017 at 11:40 am - Reply

      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.

      • Isaac Bekheit May 19, 2017 at 7:50 pm - Reply

        yes of course I got new api key also I tested on android I dont get any readings at all

  6. Isaac Bekheit May 18, 2017 at 11:20 am - Reply

    on Android I dont get any readings at all.

Leave a Reply

Show Buttons
Hide Buttons