There seems to be a perception among some people that Delphi is in the unique position of allowing developers to share and re-use code across the various platforms that it’s compiler can now (and will soon) target. But this is not the case. Oxygene has had this capability right from the start.
To exercise this, at an admittedly rudimentary level so far, I created a simple application for iOS using Oxygene Nougat. The application is very simple, consisting of two edit fields and a button. You enter two numbers into the fields, press the button and the application pops up a message with the sum of those two numbers.
I then created the same application using Oxygene Cooper, for Android.
Clearly the amount of “business logic” involved is minimal. The vast majority of this simple application resides in the UI. But the object of this exercise was to explore the sharing of generic, non-platform-specific code between different Oxygene projects.
First, here is my Adder class that will be shared across the platforms:
namespace Deltics.Adder; interface type Adder = public class public property A: Integer; property B: Integer; property Sum: Integer read A + B; end; implementation end.
As I say, this is hardly rocket science. But something worth pointing out here is the efficiency of the Oxygene syntax.
First, to declare a read-write property, such as the A and B properties in this case, you do not need to erect the backing field and specify the read/write accessors. Simply declare the property and the type. You can of course specify read-only properties, and we have one here also which demonstrates another efficiency.
The Sum property is a read-only property which returns the result of the addition of the A and B properties. Such relationships can be expressed directly with read expressions. There is no need to implement a separate property read accessor method.
The other thing to note is that “units” in Oxygene are declared as members of a namespace. i.e. rather than namespaces being fabricated from arbitrarily unique unit names, they are a formal part of the language. There is no need for each unit to have a unique “unit name” – the filename distinguishes them as separate files after all.
Just for a fun comparison, here’s the equivalent Delphi class:
unit Deltics.Adder; interface type TAdder = class private fA: Integer; fB: Integer; function get_Sum: Integer; public property A: Integer read fA write fA; property B: Integer read fA write fA; property Sum: Integer read get_Sum; end; implementation function TAdder.get_Sum: Integer; begin result := fA + fB; end; end.
Just as Oxygene is a “better C#”, it’s also a “better Delphi”. imho. π
But back to Oxygene.
I won’t bother you with the details of the applications themselves. Suffice to say that in each case I constructed the UI programmatically, as an exercise primarily in forcing myself to actually write some platform code as a learning exercise. But it should be noted that despite perceptions to the contrary in some quarters, Oxygene does have GUI building tools. Or rather, it works alongside the existing GUI building tools provided for each platform.
Did somebody say “native” ?
But I digress. Again. π
In terms of solution structure (something that I expect to evolve as I gain experience with multiple platforms and Visual Studio solution concepts in general) I arrived at the following on disk:
Adder [Solution folder] Deltics.Adder.pas iAdder [iOS project folder] org.me.adder [Android project folder]
The Deltics.Adder unit (I think it is correct to still call it a unit even though the code is not part of a formal unit declaration as such) is added to each of the iAdder and org.me.adder projects using “Add Link” (i.e. reference) so that each project references this common file rather than having separate copies (which irritatingly is the default behaviour in Visual Studio when adding an existing file to a project).
So, now to those projects. Here is the code from the iOS GUI that employs my simple Adder class:
method RootViewController.btnSumPressed; var adder: Adder; msg: UIAlertView; begin adder := new Adder; adder.A := textA.text.integerValue; adder.B := textB.text.integerValue; // Initialise our Alert View Window with options msg := UIAlertView.alloc.initWithTitle('The Sum of the Parts is...') message(adder.Sum.stringValue) delegate(self) cancelButtonTitle('OK') otherButtonTitles(NIL); msg.show; end;
And here is the equivalent code from the Android GUI:
method MainActivity.ButtonOnClick(v: View); var myAdder: Adder; msg: AlertDialog; begin myAdder := new Adder; myAdder.A := Integer.parseInt(textA.Text.toString); myAdder.B := Integer.parseInt(textB.Text.toString); msg := new AlertDialog.Builder(self).create; msg.Title := 'The Sum of the Parts is...'; msg.Message := myAdder.Sum.toString; msg.show(); end;
Now I should mention that the AlertDialog code here is incomplete. As written above it currently presents a dialog with no buttons, meaning that I have to use the Android “back” hardware key to dismiss it, but the point of this post is the sharing of code between platforms not the platform UI concerns that I am still learning (I have to say that so far iOS has been the more pleasant experience, by far).
Of course this does raise the interesting point that on iOS there is no hardware “back button” which in turn raises the question, just how exactly does anyone think they can realistically create one app from one set of source that will behave properly on different platforms that themselves work differently ?
This is of course why I chose Oxygene. Yes, it is an inconvenience having to create platform specific GUI’s. But it is also in my view necessary, and to think otherwise is frankly delusional.
But back to the issue of the day.
Let us compare the usage of the shared “business object”. First, the setting of the A and B properties:
// Nougat - iOS adder.A := textA.text.integerValue; adder.B := textB.text.integerValue; // Cooper - Android myAdder.A := Integer.parseInt(textA.Text.toString); myAdder.B := Integer.parseInt(textB.Text.toString);
This illustrates some pretty fundamental platform differences between iOS and Android, each requiring different incantations to obtain an integer from the corresponding string representation. In the case of iOS/Cocoa, the string type itself has a method to achieve this – integerValue. In the case of Android/Java, we use the parseInt method of the Integer type.
Of perhaps more interest is what happens when we are working with the Sum property, to obtain the string representation of its integer value:
// Nougat - iOS message(adder.Sum.stringValue) // Cooper - Android msg.Message := myAdder.Sum.toString;
Here we something very strange. In each case, we are working with a string type property of a class which was not written specifically for any particular platform. Yet in each case the mechanism for obtaining the string representation of that integer is different and each appears to be a method of the string type itself.
How can the same string type have different methods ? And if it is the same type, why can we not use the same method in each case ?
The answer of course is that it is not the same type at all. Both Android and iOS have types which correspond to a “String” type, and Oxygene maps these onto our property type. It is a String but when required it is an NSString or a java.lang.String.
Although the mechanism involved is different, this effect is essentially the same as the approach in Delphi taken with the String type, where even with all of the additional RTTI that makes String so useful in Delphi, it is also a pointer to a simple null terminated string (PChar) that can be used directly (in most cases) with the platform API – i.e. Win32.
The Spirit of Delphi lives on. Just these days, not in Delphi itself.
Something else that is going on that is less obvious is that when compiled for Android, my Adder class extends the Java Object class. When compiled for iOS the same class extends the Cocoa NSObject.
Did someone say “native” ? π
At this point I should apologise to anyone at RemObjects if my understanding of what is going on is not accurate.
Some people will be aghast at the implications of this. This means after all that when you are writing the code for a business object that will be shared across different platforms you must be careful to avoid any platform specific incantations in that code. That could be difficult when even such things as simple as converting between string and integer representations are platform specific operations.
This is where Sugar comes in, and where the concept of “mapped types” in Oxygene comes to our aid.
That will be the feature in my next post on Oxygene.
I can recall reading an article about the different string type “problem” some time back and to me it was a little off-putting. I get the need to created different interfaces for different platforms but I ultimately interpreted it as strings would be incompatible on different platforms as well so different classes etc would be required.
Your post suggests that this is not exactly the case in that a string is still a string, however the string operations may not map exactly. I think the article did mention Sugar but I don’t recall seeing any updates on it. I haven’t touched Oxygene since a quick dabble with Prism some years ago, and my misinterpretation of this string issue is one of the reasons that I never tried with again once cooper and Nougat came about, although I keep reading about them with interest. I want to be able to share code across platforms (at least at the object level) as much as possible.
So you have just upped my level of interest again.
You might want to take a look at the conference stuff I did at BASTA Fall 2011. It’s C#, but covers MonoTouch and MonoDroid: https://bitbucket.org/jeroenp/conferences/src/a20752f562f4c5fe248aa2f16e5841b4198734b7/2011/Basta-Fall-2011-Rheingold-Halle-Mainz?at=default Still need to find time to convert it to Pascal based environments.
What about the DB Layer? When it comes to access a database, I also have to write different code for each plattform. Do I have to setup and configure DB access classes for each plattform? Furthermore, in Delphi I have a TDataset inherited class. I can pass it into my business logic to process the data. How do I retrieve data and pass it to the business logic, or how to display it in a e.g. Grid (TDataSet -> TDataSource -> xxxDBGrid)?
All in good time.
But at this point I would simply ask, what if you didn’t have TDataSet ? Then you would have the same problem in Delphi.. So the answer is that if there is no TDataSet you – or someone – has to come up with one.
Since both Android and iOS incorporate SQLite it should – I speculate – be possible to devise a library to provide a common framework that exposes the platform specific details of the SQLite implementations in a way that can be shared. Someone may even have already done this. If they haven’t then this might be something I would enjoy turning my hand to.
A database component was the very first project I took on with Delphi 1 (an encapsulation of the Gupta SQL/API for SQLBase). There would be a certain symmetry in undertaking a similar project to cut my teeth on with Oxygene. π
Cool article. I know you want to tackle sugar later on, but I did want to add that sugar has a String type that has the same interface on all 3 platforms. It has IndexOf(), LastIndexOf(), SubString() etc. And when you use this String type, you’re really using the underlying class (it doesn’t wrap it, every operation on it is replaced with the real call when used)
In such mobile applications, most of the time is spent designing the UI and adapting it to the target platform expectations.
So with Oxygene, in practice you will probably only share about 5-10% of your code between Android and iOS.
Most of the coder time will be spent not at Oxygene level, but at platform/framework/classes level.
Direct access to the underlying platform native objects is very confusing and time consuming.There is almost nothing in common between textA.text.integerValue and Integer.parseInt(textA.Text.toString)
A thin adapter layer or compiler magic is needed here. Oxygene sounds a bit immature to me. Having direct access to the underlying object conceptual view, i.e. either ObjectiveC or Java, is IMHO not a feature, but a limitation.
There is precisely such an adapter layer, which employs mapped types which you might think of as “compiler magic”. It’s called “Sugar” and it provides a consistent API to various types but is not an adapter layer sitting on type, but an adapter which works with the compiler so the result is that you write code against the abstraction but that code is mapped by the compiler directly on to the corresponding platform specific, native code.
This is what I shall be looking at next time.
Beloved sugar!
Having direct access to the underlying platform is not a limitation. It’s not even a feature. It’s a conceptual MUST HAVE when working with different platforms.
If you have a layer in between, that abstracts you away from the platform, you’ll end up with “Einheitsbrei” – a concept I discussed in two posts in my own blog:
1.) http://dotnetninja.de/2013/04/why-firemonkey-is-so-fundamentally-wrong-in-every-aspect-of-its-being/
2.) http://dotnetninja.de/2013/07/why-firemonkey-is-wrong-the-second/
Experience with several mobile applications showed:
A mobile application that does not ‘fit in’ the behaviour of the platform will not be successful.
This ‘fit in’ is not, and this is important, restricted to the look and feel of single controls, but it must fit in the global user experience of the platform. Just like the hardware button control ability on Android and the buttonless dialog mentioned in this post here.
As an example, there was an application that used phone gap and mimicked – not really good – the iOS user experience on all platforms. It had an ‘okay’ success on iOS, but failed completely on Android and Windows Phone. All three apps together didn’t pay out enough money to make up the common development costs.
I strongly talked to the managers that – even with additional efforts – this concept will fail, and they would need to start over with a separate app on each platform to succeed. In the end, they agreed.
So, there was a native app in Objective-C for iOS, a Java Android app and a HTML5/Javascript App for Windows Phone that re-used a bit of the phone gap code but completely re-did the UX. Development costs were of course higher, but each app costed a little less than the shared phone gap app, because testing was restricted to one platform.
Nevertheless: Each native app payed out more than it’s own development costs just two months after publishing it’s update.
Einheitsbrei will not succeed, and an abstraction layer that forces you to produce einheitsbrei is not a feature, but a limitation.
Without mention of the βSugarβ, this post would be “Fail of the month” in my personal rating π
I find that before someone can appreciate a solution it is often necessary for them to properly grasp the “problem”. π
I enquote “problem” because I believe that the affinity with the different platforms is an essential feature of the Oxygene approach, which is why I wanted to highlight it, but it is important to also recognise the difficulties this can present but then having done so, present the – or a – solution to at least some of those difficulties.
Shrug. Before Delphi for Android, I could write C++ code that is shared across iOS and Android, and in a while (if not already) use Qt on both, and get better performance to boot. Or use C# via Xamarin, if I am keen on interpreters. And now I could use Delphi.
New rule: mentioning C#/Xamarin and “interpreters” in the same sentence immediate qualifies you for the bozo in, as you just proved your total ignorance of how things actually works, and are just reading off the Embarcadero buzzword bingo.
Bad news as I believed only UI code needs to be different. Now it seems only the syntax is shared while no code can be, if I’m getting this right.
I really hope they roll out some shared stdlib/types at least.
No, you’re not getting it right at all. π
The VCL/RTL is to Delphi as platform UI/Sugar is to Oxygene. Remember that “VCL” is a platform UI, it’s just that the only platform it supports is Win32/64.
Also, what makes Sugar work is a sight more sophisticated than simply a bunch of runtime library functions. Many types and operations are mapped directly onto the underlying platform “intrinsics”, not an abstracted wrapper around/on top of them.
As I said at the close of this post, I’ll be covering Sugar next. Stay tuned. π