Today Microsoft made an announcement that Windows 10 will allow Android applications to run in an Android AOSP subsystem. Additionally, Objective-C applications can be compiled and run on Windows 10 as well as universal applications. These announcements, particularly the later, surprised and excited many of us. I've been speaking with a lot of people both inside and outside of Magenic on what all of this means, or could possibly mean, given our limited understanding. (For details See Kevin McLaughlin's article and Mary Jo Foley's article)
First there are some pretty important technical questions raised by this:
- What will the applications look like in Windows 10? If the Android applications are just running in a ASOP subsystem then presumably you would have a bunch of applications looking and navigating like native Android applications on your Windows 10 device. That may be OK for some applications but probably not others.
- How about access to system services and 3D rendering? If I need access to the hardware GPS can one application running in the Android subsystem do that while a Windows 10 application tries to get at it at the same time. This could be handled but I'm curious as to the level to which it is handled or if there be dragons.
- For Objective-C what if my application is accessing HealthKit? I assume HealthKit is not available in Windows 10 when I port my code. When running an Android application how about Google services, can I install them in the subsystem?
- It seems Apple is putting a huge effort behind Swift but MS only mentioned Objective-C; will there be Swift support some time in the future? I just finished writing a white paper on when to use Objective-C vs. Swift. Until and unless MS also supports doing this with Swift, this could change the equation for applications that ever may be expected to run on Windows 10. For that concern, Objective-C would be the one that could work.
- I've heard that Objective-C apps may support porting over Xibs but not Storyboards. If true that could limit the applications that can be ported over without refactoring.
I look forward to learning more and getting some answers to some of these questions. Early days! :)
Even with all those questions, there are a few conclusions that we can come to with what we currently know today.
- One question I've seen floating around is do we still need solutions like Xamarin or NativeScript. The answer is absolutely yes. These new capabilities allow us to more easily run Android applications written in Java to our Windows 10 devices and compile Objective-C code into unified applications for Windows 10. What they don't do is allow you to run your applications written in Objective-C on an Android device or in Java on iOS. For this type of cross platform work you need a different cross platform tool. Then there is the question of will it look right on the platform? Technologies like Xamarin and NativeScript solve that problem, this will not.
- I've heard a comparison with when OS/2 was designed to run Windows applications to help capture the market from Windows and it did it poorly with the conclusion that Microsoft should expect the same result with Android and iOS apps running on Windows 10. Does this argument have any legs? Maybe in some ways, but not too much of a worry. OS/2 failed for many reasons, many of which had nothing to do with having Windows compatibility. But there is one area in particular where the analogy partially seems to hold true, handheld mobile devices. Like OS/2, Windows is not the dominant player in this space and is fighting entrenched leaders. But even then I don't give the argument too many legs. Like OS/2, Windows 10 will will likely stand or fall based on its strengths in the tablet space and will have an uphill battle in the mobile space just because there is so much momentum behind the leaders. If there is any detrimental impact of Android or Objective-C code on Windows 10 will be small next to that. Given the potential upside of additional applications, certainly a viable and likely small risk.
- Applications that rely on platform specific services like HealthKit or Google Play Services are unlikely to just work. The upshot of the deal, if you are writing an Objective-C or Android Java applications that accesses those services then all of the cross platform concerns come into play, just like they do writing a Xamarin application. Suddenly you have to think clearly about things like inversion of control patterns around platform specific services and writing a different implementation for the Windows platform that may change or even disable functionality.
- Microsoft has made a big deal of giving away Windows 10 and trying to get people to move. Now we see part of the reason as these capabilities are unlikely to be back ported to Windows 8.1 or Windows Phone. So the quicker users move the better. However, the enterprise is notoriously slow to move. How much this lag will carry over to the tablet space remains to be seen but it has been a much faster cycle for mobile phones.
- This move has obviously been made because it is very difficult to attract developers to a platform when there are few users and users to a platform when there are few applications. By lowering the bar to run many existing applications on Windows 10 they have made moves to help solve that equation and completely changed the price point to entry. However, this does have its limits. If I'm currently writing a native Android or iOS app, getting it to work on Windows is still not free. If I'm very lucky all I'm looking at is increased platform testing and perhaps compilation costs in terms of Objective-C. If I'm not lucky I'm re-architecting portions of my application to abstract out capabilities and services I can't access or otherwise don't work on Windows 10. For public facing apps on smart phone devices, software companies will still have to determine if that reduced cost is worth the 3% of US smart phone marketshare. Such investment may be much easier to justify in places like Europe where Windows Phone holds a 10% share.
Final thoughts
If this works well it will undoubtedly increase the attractiveness of the Windows 10 platform, particularly in the tablet space. No matter how you cut it, it is just another example in a long line of examples of how Microsoft has changed and become a very different company than it was a few short years ago. Microsoft continues to show us that it is a fighter and willing to make fundamental changes to stay relevant.
I feel great knowing that I can find enhanced utility in my ability to write Java and Objective-C. However, for my clients that need cross platform applications they almost always are looking at the two big smart phone players; Android and iOS. For that, I will continue to use tools like Xamarin and .Net.
I do wonder if this portends some future movement of MS into Xamarin's space. There were talks of acquisition last year that never materialized. Xamarin is a small player and that can work if they can hide in a niche. I see MS and Xamarin working more an more together so Microsoft is definitely getting interested in that niche. I know a lot of guys at Xamarin and they are a nice and passionate bunch that deserve credit and reward for what they have done to advance the cause and capabilities of X-Platform mobile development. No matter how this turns out I hope they receive the recognition and reward they deserve for their pioneering work.
This move by Microsoft makes a large shift in how I think about the different platforms and their capabilities and I look forward to continuing to evaluate the impact of this as we know more.
Wednesday, April 29, 2015
Sunday, March 22, 2015
Starting with Microsoft Band and Xamarin
Recently Xamarin released their API for Microsoft Band that works with Android, iOS and Windows Phone. It was a tough choice on what to play with before vacation; a new Xamarin Forms preview that works with Windows Store and Windows Phone 8.1 was released as well as Xamarin Everywhere that hits Android Wear and Apple Phone. A complete overload of super cool stuff.
But ... last week I got a new Microsoft Band and you know, time to play. First off, the Band isn't really a Smart Watch. It's more a like a fitness device on steroids. You can pull all kinds of sensor information out of it, you can also send notifications to it and play with it's appearance. What you can't really do is input information into the Band and have to do something on the phone. That is to say the band is about collecting sensor information and consuming information but not for inputting information like a "true" smart watch allows.
Xamarin has wrapped the Microsoft platform APIs, pretty much verbatim. One thing they have done which is nice is if the calls are asynchronous on Android, they have Task returning versions. However, like the Microsoft platform specific versions the APIs do not match from platform to platform so making cross platform code will still be challenging unless you wrap them in your own unified interface. Xamarin has a nice page on the component store that explains the basics of the APIs which are pretty easy to use. They can be found here:
https://components.xamarin.com/view/microsoft-band-sdk
With that in mind I set out to give this a try. Since my Band is currently synchronized with an Android device I am working with the Android version of the API. First I figured I'd try to add and then remove an icon from my Band that could receive notifications and messages. I created a class to open and close the connection.
The class just takes in the current activity and then gets the first band registered with the device. If you were making a "real" application it may be a good idea to make some sort of screen to choose what Band the user wanted to connect to if there were more than one. The ConnectAsync method opens the connection to the Band. As you can see I use the ConnectTaskAsync method of the BandClient instead of ConnectAsync. All the asynchronous Android APIs seem to be wrapped up with Task returning operations with that naming convention. When the class is disposed, if the bandClient is connected, the connection is closed.
I added a simple method to create a new tile on my band if there is room for one.
I have added two images to my Android project, as per the API specs. One image is 24x24 and the other 48x48. I create them and convert them into the BandIcon format. The following code is what I used to call the AddTileAsync method.
When using this code it automatically sinks up to the device. The tile is identified by a unique identifier that I added as a constant to my code. When running it will first ask the user if they want to add the new tile to the Band. You can see my "awesome" abilities as an icon designer:
After choosing allow the band synchronizes the new tile onto the Band.
By scrolling the tiles on the band the newly added tile appears.
I then added a method to remove the tile I just added.
This worked without incident and the tile from my band went away.
Sending a message to the band was also simple. The API reference contains the following description on the difference between a message and a dialog:
App notifications come in two flavors:
- Dialogs – dialog notifications are popups meant to quickly display information to the user. Once the user dismisses the dialog, the information contained therein does not persist on the Band.
- Messages – message notifications are sent and stored in a specific tile, and a tile can keep up to 8 messages at a time. Messages may display a dialog as well.
I added this method to my class to create a message:
This method calls into my ShowMessageAsync method and gets the device location, converts it to an address and displays it to the user.
The message comes across to the band as expected.
Additionally, as expected, a message indicator appears with our tile stating that one new message was received.
When selecting the tile the message appears fully (I blocked out my address).
Sending a dialog is similarly easy. But as this test indicates the amount of text that can be displayed in the dialog is limited and there is no down button (see more) as with the message. Dialogs should only be used for very short notifications.
When sending this over it shows up as a popup but does not add to the message counter or add to the message history. As the API documentation stated, the dialog is not retained.
That's it for my first go. Vibrating the band was similarly easy. Next I'll poke around at getting data from the Band and perhaps customizing its appearance. Generally I was pleased at how easy it was to work with but cross platform applications may want to create a unified interface.
But ... last week I got a new Microsoft Band and you know, time to play. First off, the Band isn't really a Smart Watch. It's more a like a fitness device on steroids. You can pull all kinds of sensor information out of it, you can also send notifications to it and play with it's appearance. What you can't really do is input information into the Band and have to do something on the phone. That is to say the band is about collecting sensor information and consuming information but not for inputting information like a "true" smart watch allows.
Xamarin has wrapped the Microsoft platform APIs, pretty much verbatim. One thing they have done which is nice is if the calls are asynchronous on Android, they have Task returning versions. However, like the Microsoft platform specific versions the APIs do not match from platform to platform so making cross platform code will still be challenging unless you wrap them in your own unified interface. Xamarin has a nice page on the component store that explains the basics of the APIs which are pretty easy to use. They can be found here:
https://components.xamarin.com/view/microsoft-band-sdk
With that in mind I set out to give this a try. Since my Band is currently synchronized with an Android device I am working with the Android version of the API. First I figured I'd try to add and then remove an icon from my Band that could receive notifications and messages. I created a class to open and close the connection.
public class BandConnection : IDisposable
{
private IBandClient bandClient;
private Activity context;
public BandConnection(Activity context)
{
this.context = context;
var pairedBands = BandClientManager.Instance.GetPairedBands();
if (pairedBands != null && pairedBands.Length >= 1)
{
bandClient = BandClientManager.Instance.Create(context, pairedBands[0]);
}
}
public async Task ConnectAsync()
{
if (bandClient == null)
{
throw new InvalidOperationException("BandClient is not set");
}
await bandClient.ConnectTaskAsync();
}
public async void Dispose()
{
if (bandClient != null && bandClient.IsConnected)
{
await bandClient.DisconnectTaskAsync().ConfigureAwait(false);
}
}
}
The class just takes in the current activity and then gets the first band registered with the device. If you were making a "real" application it may be a good idea to make some sort of screen to choose what Band the user wanted to connect to if there were more than one. The ConnectAsync method opens the connection to the Band. As you can see I use the ConnectTaskAsync method of the BandClient instead of ConnectAsync. All the asynchronous Android APIs seem to be wrapped up with Task returning operations with that naming convention. When the class is disposed, if the bandClient is connected, the connection is closed.
I added a simple method to create a new tile on my band if there is room for one.
public async Task AddTileAsync(string tileId, string tileName, BandIcon largeIcon, BandIcon smallIcon)
{
if (bandClient == null)
{
throw new InvalidOperationException("BandClient is not set");
}
var tiles = await bandClient.TileManager.GetTilesTaskAsync().ConfigureAwait(false);
var id = UUID.FromString(tileId);
var spacesRemaining = await bandClient.TileManager.GetRemainingTileCapacityTaskAsync().ConfigureAwait(false);
if (spacesRemaining > 0 && !tiles.Any(t => t.TileId.ToString() == tileId))
{
var tileBuilder = new BandTile.Builder(id, tileName, largeIcon);
if (smallIcon != null)
{
tileBuilder.SetTileSmallIcon(smallIcon);
}
var tile = tileBuilder.Build();
await bandClient.TileManager.AddTileTaskAsync(context, tile);
}
}
I have added two images to my Android project, as per the API specs. One image is 24x24 and the other 48x48. I create them and convert them into the BandIcon format. The following code is what I used to call the AddTileAsync method.
using (var bandConnection = new BandConnection(this))
{
if (bandConnection.BandClient != null)
{
await bandConnection.ConnectAsync().ConfigureAwait(false);
var smallIconBitmap = ((BitmapDrawable)Resources.GetDrawable(Resource.Drawable.PhoneFinderSmall)).Bitmap;
var smallIcon = BandIcon.ToBandIcon(Bitmap.CreateScaledBitmap(smallIconBitmap, 24, 24, false));
var largeIconBitmap = ((BitmapDrawable)Resources.GetDrawable(Resource.Drawable.PhoneFinderLarge)).Bitmap;
var largeIcon = BandIcon.ToBandIcon(Bitmap.CreateScaledBitmap(largeIconBitmap, 48, 48, false));
await bandConnection.AddTileAsync(BandConstants.TileId, "Band Test", largeIcon, smallIcon).ConfigureAwait(true);
}
}
When using this code it automatically sinks up to the device. The tile is identified by a unique identifier that I added as a constant to my code. When running it will first ask the user if they want to add the new tile to the Band. You can see my "awesome" abilities as an icon designer:
After choosing allow the band synchronizes the new tile onto the Band.
By scrolling the tiles on the band the newly added tile appears.
I then added a method to remove the tile I just added.
public async Task RemoveTileAsync(string tileId)
{
if (bandClient == null)
{
throw new InvalidOperationException("BandClient is not set");
}
var tiles = await bandClient.TileManager.GetTilesTaskAsync().ConfigureAwait(false);
var id = UUID.FromString(tileId);
if (tiles.Any(t => t.TileId.ToString() == tileId))
{
await bandClient.TileManager.RemoveTileTaskAsync(id).ConfigureAwait(false);
}
}
This worked without incident and the tile from my band went away.
Sending a message to the band was also simple. The API reference contains the following description on the difference between a message and a dialog:
App notifications come in two flavors:
- Dialogs – dialog notifications are popups meant to quickly display information to the user. Once the user dismisses the dialog, the information contained therein does not persist on the Band.
- Messages – message notifications are sent and stored in a specific tile, and a tile can keep up to 8 messages at a time. Messages may display a dialog as well.
I added this method to my class to create a message:
public async Task ShowMessageAsync(string tileId, string title, string body, DateTime date, bool showMessage)
{
if (bandClient == null)
{
throw new InvalidOperationException("BandClient is not set");
}
var id = UUID.FromString(tileId);
await bandClient.NotificationManager.SendMessageTaskAsync(id, title, body, date, showMessage);
}
This method calls into my ShowMessageAsync method and gets the device location, converts it to an address and displays it to the user.
using (var bandConnection = new BandConnection(this))
{
if (bandConnection.BandClient != null)
{
await bandConnection.ConnectAsync().ConfigureAwait(false);
var geocoder = new Geocoder(this);
var addressList = await geocoder.GetFromLocationAsync(_currentLocation.Latitude, _currentLocation.Longitude, 10).ConfigureAwait(false);
var address = addressList.FirstOrDefault();
if (address != null)
{
var deviceAddress = new StringBuilder();
for (int i = 0; i < address.MaxAddressLineIndex; i++)
{
deviceAddress.Append(address.GetAddressLine(i))
.AppendLine(",");
}
await bandConnection.ShowMessageAsync(BandConstants.TileId,
"Test Message",
string.Format("Current address: {0}", deviceAddress),
DateTime.Now,
true).ConfigureAwait(true);
}
}
}
The message comes across to the band as expected.
Additionally, as expected, a message indicator appears with our tile stating that one new message was received.
When selecting the tile the message appears fully (I blocked out my address).
Sending a dialog is similarly easy. But as this test indicates the amount of text that can be displayed in the dialog is limited and there is no down button (see more) as with the message. Dialogs should only be used for very short notifications.
public async Task ShowDialogAsync(string tileId, string title, string body)
{
if (bandClient == null)
{
throw new InvalidOperationException("BandClient is not set");
}
var id = UUID.FromString(tileId);
await bandClient.NotificationManager.ShowDialogTaskAsync(id, title, body);
}
When sending this over it shows up as a popup but does not add to the message counter or add to the message history. As the API documentation stated, the dialog is not retained.
That's it for my first go. Vibrating the band was similarly easy. Next I'll poke around at getting data from the Band and perhaps customizing its appearance. Generally I was pleased at how easy it was to work with but cross platform applications may want to create a unified interface.
Tuesday, March 3, 2015
Direct casting vs. As and Linq Single, SingleOrDefault, First, FirstOrDefault
There are tons of articles on these two topics but I still see it so many times I'm putting out a quick reminder.
This:
Or this:
I come across people doing the first when they should be direct casting (the second) all the time in code reviews. When I come across the first the first thing I look for, somewhere very soon after in the code, is a null check. Why? because using the syntax of the first means that you expect that someOtherVariable may be not castable to SomeType and when that happens someVariable will be set to null. That is to say that since it may be something other than SomeType, you need to handle that case in code.
If however, someOtherVariable should always have at value that is castable to SomeType and anything else is a logic error somewhere in the code, then you should be using direst casting. Direct casting will throw an exception right then and there if it is not SomeType and that's what you want. Why? Because somewhere in your code you have a logic error and you want to find out about it as soon as possible. Pretty simple to remember.
OK, what are the differences between these Linq statements:
Here is my easy way to remember:
First: ( 1 .. n) It is valid to have one or more results and you want/get the first. If there are no results that is a logic error and should throw an exception.
FirstOrDefault: ( 0 .. n) It is valid to have zero, one or more valid results and you want/get the first. Generally if you have no valid result someVariable will be set to null, similar to the "as" syntax above.
Single: ( 1 .. 1 ) It is valid to have one and only one result. Anything else (zero or more than one) is considered a logic error and will throw an exception. Great for unique Ids when you know the record should be in the collection
SingleOrDefault: ( 0 .. 1): There should be zero or one result, more than one matching result will throw an exception. Generally if you have no valid result someVariable will be set to null, similar to the "as" syntax above. Great for unique Ids when you don't know if the record has been added to the collection and if it isn't in there, you need to add it.
I see these misused all the time too, particularly people favoring FirstOrDefault. When I've asked them about it misuse usually comes from not understanding the statements or aversion to throwing an exception (i.e they think to themselves, but what if there are no, or many results).
The aversion to throwing an exception in these cases is wrong headed thinking. Yes, exceptions that get to the user are bad, but logic errors can be similarly bad. Even worse is logic errors that you don't catch immediately that lead to exceptions later on and now you have the unenviable task of trying to trace back and figure out many lines of code before your variable was set to null in a situation that never should have happened and you didn't handle.
Using the "as" syntax or FirstOrDefault out of a desire to avoid exceptions is a bad practice similar to swallowing exceptions. Don't do it. Fail right away and you as the developer will likely find the problem, and if you don't QA has a better chance to than if you simply don't throw and exception and keep going. By swallow the problem by using "as" or FirstOrDefault you increases the chance that the person finding an issue will be the end user and you will likely have a harder time tracking down the problem and some egg on your face.
This:
var someVariable = someOtherVariable as SomeType;
Or this:
var someVariable = (SomeType)someOtherVariable;
I come across people doing the first when they should be direct casting (the second) all the time in code reviews. When I come across the first the first thing I look for, somewhere very soon after in the code, is a null check. Why? because using the syntax of the first means that you expect that someOtherVariable may be not castable to SomeType and when that happens someVariable will be set to null. That is to say that since it may be something other than SomeType, you need to handle that case in code.
If however, someOtherVariable should always have at value that is castable to SomeType and anything else is a logic error somewhere in the code, then you should be using direst casting. Direct casting will throw an exception right then and there if it is not SomeType and that's what you want. Why? Because somewhere in your code you have a logic error and you want to find out about it as soon as possible. Pretty simple to remember.
OK, what are the differences between these Linq statements:
var someVariable = someOtherVariable.First(r => r.SomeValue == "value");
var someVariable = someOtherVariable.FirstOrDefault(r => r.SomeValue == "value");
var someVariable = someOtherVariable.Single(r => r.SomeValue == "value");
var someVariable = someOtherVariable.SingleOrDefault(r => r.SomeValue == "value");
Here is my easy way to remember:
First: ( 1 .. n) It is valid to have one or more results and you want/get the first. If there are no results that is a logic error and should throw an exception.
FirstOrDefault: ( 0 .. n) It is valid to have zero, one or more valid results and you want/get the first. Generally if you have no valid result someVariable will be set to null, similar to the "as" syntax above.
Single: ( 1 .. 1 ) It is valid to have one and only one result. Anything else (zero or more than one) is considered a logic error and will throw an exception. Great for unique Ids when you know the record should be in the collection
SingleOrDefault: ( 0 .. 1): There should be zero or one result, more than one matching result will throw an exception. Generally if you have no valid result someVariable will be set to null, similar to the "as" syntax above. Great for unique Ids when you don't know if the record has been added to the collection and if it isn't in there, you need to add it.
I see these misused all the time too, particularly people favoring FirstOrDefault. When I've asked them about it misuse usually comes from not understanding the statements or aversion to throwing an exception (i.e they think to themselves, but what if there are no, or many results).
The aversion to throwing an exception in these cases is wrong headed thinking. Yes, exceptions that get to the user are bad, but logic errors can be similarly bad. Even worse is logic errors that you don't catch immediately that lead to exceptions later on and now you have the unenviable task of trying to trace back and figure out many lines of code before your variable was set to null in a situation that never should have happened and you didn't handle.
Using the "as" syntax or FirstOrDefault out of a desire to avoid exceptions is a bad practice similar to swallowing exceptions. Don't do it. Fail right away and you as the developer will likely find the problem, and if you don't QA has a better chance to than if you simply don't throw and exception and keep going. By swallow the problem by using "as" or FirstOrDefault you increases the chance that the person finding an issue will be the end user and you will likely have a harder time tracking down the problem and some egg on your face.
Wednesday, February 4, 2015
Mobile Development Platform Performance Part 2 (Native, Cordova, Classic Xamarin, Xamarin.Forms)
This is the second in the series comparing sample test applications using vendor native development technologies (Objective-C, Java), Cordova (using Microsoft's Multi-Device Hybrid Apps), classic Xamarin and Xamarin Forms. This time I decided to focus on IO using the different frameworks. For this I used SqLite to save and retrieve records and also just writing and reading from plain text files.
For some background on testing methodology using Android and iOS.
The Development Platforms
- Native (Objective-C 64 bit and Java)
- Cordova (Multi-Device Hybrid Apps) using Intel's App Framework for the UI
- Classic Xamarin (64 bit unified beta for iOS)
- Xamarin.Forms (64 bit unified beta for iOS with Xamarin Forms version 1.3.1)
The Devices
- iPad Mini (non Retina) running iOS 8.1.1 (12B435)
- ASUS K00F running Android 4.2.2
The Test Apps
Applications were made for each of the development platforms that are functionally similar. There was little (if no) effort to make them look exactly the same or even look "good". But they looked about the same.
The Timing Methodology
Due to difficulties in knowing when things are "done", particularly with JavaScript, timings were handled via stopwatch. Each timing was taken ten times and the results were averaged. It should noted that hand timings have an accuracy of about 2/10 of a second so that does give us an approximate margin of error. In the previous post I showed the 10 individual timings and the average. This time to save me some typing I'm just showing the average of the ten timings.
Test 1: Test App Size
As mentioned in the last set of test, the size of the application can impact how much bandwidth it takes to deploy and also have some impact on load times. For Android the size of the APKs was examined. For iOS I looked ipa files for ad-hoc deployment.
A few interesting things to look at here. First is the Cordova apk is actually smaller than the vendor native tools on Android. I suspect this has to do with differences in size of the SqLite libraries for both of the platforms. Also while the Xamarin APKs are larger, they are not as large as they were on the first set of tests. For this I used the linking option of Link All Assemblies. This was probably important for Xamarin Forms which is, of course, just a library on top on Classic Xamarin. For whatever reason I was not able to get the Link All Assemblies option to stay with Xamarin iOS and you see the results with Link SDK assemblies only instead.
Test 2: Load Times
Like the last test I verified how long the applications took to load into memory. The results were similar to the set of tests I did previously.
As last time vendor native technologies load the fastest. Xamarin Classic follows closely with Xamarin Forms somewhat after that. In all cases, Cordova is the slowest loading. It is interesting that on iOS Xamarin Forms loaded almost as slowly as the Cordova app. Of course for iOS, nothing really took that long to load.
Test 3: Adding 1,000 records to SqLite
I originally wanted to add 10,000 records but found that too slow on my Android device and also it didn't scroll well under Cordova where my implementation had no auto paging. Offline storage is a common need with mobile applications and SqLite is one of the few solutions that span all platforms.
* I have had a few comments that there are better performing ways to do this test. For example I could have inserted 1,000 records in the same transaction or if I wanted to do it one one record in one transaction at a time, I could have done it asynchronously. Both of these points are undoubtedly true. Through SqLite you can get 1,000 records into the local database in much quicker ways on all platforms than this tests indicates. What this test is doing is looking at synchronously saving a single record in a single implicit transaction and doing that 1,000 times for all platforms. It would probably not be too difficult to do the other tests as suggested and they may be the source of a future post. Thank you to everyone who commented.
Java:
Objective-C:
JavaScript:
Xamarin (All Versions):
Xamarin Android Alternate:
*results in seconds
Right off the bat you might notice that Xamarin on Android did poorly. Very poorly. Slower than native Android or Cordova by a large margin. One thing I noticed is that in native Android I used the SQLiteOpenHelper but this is only available in the Android API. I used the method I did because it was cross platform and you can see the results on Android. SQLiteOpenHelper is available in the SqLite Xamarin Library for Android and on a hunch I tried it instead and you can see those timings under Xamarin Classic Alternate for Android and the timing were virtually the same as they were for Java. I suspect if I used this for Xamarin.Forms I would have gotten about the same advantages. The lesson from this is just because there is a cross platform version of the API, it doesn't mean it will perform well. Buyer beware.
Test 4: Querying SqLite Records
I wanted to test reading the 1,000 records I just wrote. I only did the 1,000 I originally wrote because I didn't want to deal with paging solutions for Cordova. A 10,000 record test performed very poorly when just creating a 10,000 for HTML table.
Java:
Objective-C:
JavaScript:
Xamarin (All Versions):
Xamarin Android Alternate:
*results in seconds
Reading records in Android was similarly bad for Xamarin on Android until I switched to the SQLiteOpenHelper implementation and then performance came online with the native example. As usual the interpreted JavaScript in Cordova slower but probably won't be a factor unless there is a large operation reading thousands of lines.
Test 5: Writing Lines to a File
I also wanted to test writing lines to a text file and how that performs. For each platform I write 1,000 lines to a file.
Java:
Objective-C:
JavaScript:
Xamarin (All Versions):
*results in seconds
When it comes to writing lines to a text file the vendor native technologies are the undisputed leaders in high performance. Xamain on iOS is close and a little slower on Android. Cordova comes in at 3-4 times slower than the other technologies. Some of this may come from the interpreted looping logic and not the plugin but the reality is that the overhead of the interpreted code that calls into the plug happens if you write one line or a thousand.
Test 6: Reading Lines from a File
Compliment to test 5, reading those 1,000 lines from a file and displaying them on a list.
Java:
Objective-C:
JavaScript:
Xamarin (All Versions):
*results in seconds
You want fast, go for native. Xamarin is as good or nearly as good except for Form on Android. This may have more to do with the performance of the list display than the actual reading of the list. That should be comparable to classic Xamarin. Same comments on Cordova as before, it's slower. If this is a factor or not depends a lot about the type of application.
I hope some of you find these comparisons helpful. The lessons I learned from this set of tests were:
- It is self-evident that not all implementations of code even on the same development platform perform the same. Having said that, be careful of wrappers around libraries that have unified APIs to allow for cross platform code. It may not perform as well as the specialized libraries for Android or iOS.
- While it is hard to differentiate slowness in code that comes from it being interpreted and what comes from the libraries they are using, to some extent it doesn't matter. In production apps the Cordova JavaScript code will be interpreted as it calls into plugins like SqLite so both will be a factor.
- The linking settings on Xamarin can have a huge difference in the size of the app. If you are using external libraries where you are not likely to use all the functionality use the Link All Assemblies option if possible.
Thanks everyone.
Source code for the tests can be found here: Performance Tests Source
For some background on testing methodology using Android and iOS.
The Development Platforms
- Native (Objective-C 64 bit and Java)
- Cordova (Multi-Device Hybrid Apps) using Intel's App Framework for the UI
- Classic Xamarin (64 bit unified beta for iOS)
- Xamarin.Forms (64 bit unified beta for iOS with Xamarin Forms version 1.3.1)
The Devices
- iPad Mini (non Retina) running iOS 8.1.1 (12B435)
- ASUS K00F running Android 4.2.2
The Test Apps
Applications were made for each of the development platforms that are functionally similar. There was little (if no) effort to make them look exactly the same or even look "good". But they looked about the same.
The Timing Methodology
Due to difficulties in knowing when things are "done", particularly with JavaScript, timings were handled via stopwatch. Each timing was taken ten times and the results were averaged. It should noted that hand timings have an accuracy of about 2/10 of a second so that does give us an approximate margin of error. In the previous post I showed the 10 individual timings and the average. This time to save me some typing I'm just showing the average of the ten timings.
Test 1: Test App Size
As mentioned in the last set of test, the size of the application can impact how much bandwidth it takes to deploy and also have some impact on load times. For Android the size of the APKs was examined. For iOS I looked ipa files for ad-hoc deployment.
Development Platform | Size |
---|---|
Android | |
Java | 921kb |
Cordova | 417kb |
Classic Xamarin | 2.1mb |
Xamarin.Forms | 3.4mb |
iOS | |
Objective-C (64 bit) | 55kb |
Cordova | 621kb |
Classic Xamarin | 2.95mb |
Xamarin.Forms | 8.17mb |
A few interesting things to look at here. First is the Cordova apk is actually smaller than the vendor native tools on Android. I suspect this has to do with differences in size of the SqLite libraries for both of the platforms. Also while the Xamarin APKs are larger, they are not as large as they were on the first set of tests. For this I used the linking option of Link All Assemblies. This was probably important for Xamarin Forms which is, of course, just a library on top on Classic Xamarin. For whatever reason I was not able to get the Link All Assemblies option to stay with Xamarin iOS and you see the results with Link SDK assemblies only instead.
Test 2: Load Times
Like the last test I verified how long the applications took to load into memory. The results were similar to the set of tests I did previously.
Development Platform | Test Avg. |
---|---|
Android | |
Java | 1.044 |
Cordova | 4.068 |
Classic Xamarin | 1.689 |
Xamarin.Forms | 3.032 |
iOS | |
Objective-C | 1.14 |
Cordova | 2.138 |
Classic Xamarin | 1.204 |
Xamarin.Forms | 1.928 |
As last time vendor native technologies load the fastest. Xamarin Classic follows closely with Xamarin Forms somewhat after that. In all cases, Cordova is the slowest loading. It is interesting that on iOS Xamarin Forms loaded almost as slowly as the Cordova app. Of course for iOS, nothing really took that long to load.
Test 3: Adding 1,000 records to SqLite
I originally wanted to add 10,000 records but found that too slow on my Android device and also it didn't scroll well under Cordova where my implementation had no auto paging. Offline storage is a common need with mobile applications and SqLite is one of the few solutions that span all platforms.
* I have had a few comments that there are better performing ways to do this test. For example I could have inserted 1,000 records in the same transaction or if I wanted to do it one one record in one transaction at a time, I could have done it asynchronously. Both of these points are undoubtedly true. Through SqLite you can get 1,000 records into the local database in much quicker ways on all platforms than this tests indicates. What this test is doing is looking at synchronously saving a single record in a single implicit transaction and doing that 1,000 times for all platforms. It would probably not be too difficult to do the other tests as suggested and they may be the source of a future post. Thank you to everyone who commented.
Java:
public void addRecord(String firstName, String lastName, int index, String misc) throws Exception {
if (dbConn == null) {
openConnection();
}
ContentValues values = new ContentValues();
values.put("firstName", firstName);
values.put("lastName", lastName + index);
values.put("misc", misc);
dbConn.insertOrThrow(TABLE_NAME, null, values);
}
Objective-C:
- (void)addRecord:(NSString*)firstName withLastName:(NSString*)lastName withIndex:(int)index withMisc:(NSString*)misc withError:(NSError**)error {
NSString *sqlStatement = NULL;
char *errInfo;
*error = nil;
if (dbConn == nil) {
[self openConnection:error];
return;
}
sqlStatement = [NSString stringWithFormat:@"%@%@%@%@%d%@%@%@", @"INSERT INTO testTable (firstName, lastName, misc) VALUES ('", firstName, @"', '", lastName, index, @"', '", misc, @"')"];
int result = sqlite3_exec(dbConn, [sqlStatement UTF8String], nil, nil, &errInfo);
if (result != SQLITE_OK) {
NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary];
[errorDetail setValue:[NSString stringWithFormat:@"%@%s", @"Error writing record to database: ", errInfo] forKey:NSLocalizedDescriptionKey];
*error = [NSError errorWithDomain:@"testDomain" code:101 userInfo:errorDetail];
}
}
JavaScript:
function addRecords() {
$.ui.blockUI();
var maxValue = 999;
var successCount = 0;
var db = window.sqlitePlugin.openDatabase({ name: "testDB.db" });
for (var i = 0; i <= maxValue; i++) {
var lastName = "person" + i.toString();
db.executeSql("INSERT INTO testTable (firstName, lastName, misc) VALUES (?,?,?)", ["test", lastName, "12345678901234567890123456789012345678901234567890"], function (res) {
successCount++;
if (successCount === maxValue) {
$.ui.popup({
title: "Success",
message: "All records written to database",
doneText: "OK",
cancelOnly: false
});
$.ui.unblockUI();
}
}, function (e) {
$.ui.popup({
title: "Error",
message: "An error has occurred adding records: " + e.toString(),
doneText: "OK",
cancelOnly: false
});
$.ui.unblockUI();
return;
});
}
}
Xamarin (All Versions):
public void AddRecord(string fName, string lName, int i, string m)
{
if (dbConn == null)
{
OpenConnection();
}
var testRecord = new TestTable {firstName = fName, id = 0, lastName = lName + i, misc = m};
dbConn.Insert(testRecord);
}
Xamarin Android Alternate:
public void AddRecord(string fName, string lName, int i, string m)
{
if (dbConn == null)
{
OpenConnection();
}
var testRecord = new TestTable {firstName = fName, id = 0, lastName = lName + i, misc = m};
dbConn.Insert(testRecord);
}
Development Platform | Test Avg. |
---|---|
Android | |
Java | 18.569 |
Cordova | 24.126 |
Classic Xamarin | 32.55 |
Xamarin.Forms | 30.873 |
Xamarin Classic Alternate | 18.341 |
iOS | |
Objective-C | 8.044 |
Cordova | 14.944 |
Classic Xamarin | 8.151 |
Xamarin.Forms | 8.137 |
Right off the bat you might notice that Xamarin on Android did poorly. Very poorly. Slower than native Android or Cordova by a large margin. One thing I noticed is that in native Android I used the SQLiteOpenHelper but this is only available in the Android API. I used the method I did because it was cross platform and you can see the results on Android. SQLiteOpenHelper is available in the SqLite Xamarin Library for Android and on a hunch I tried it instead and you can see those timings under Xamarin Classic Alternate for Android and the timing were virtually the same as they were for Java. I suspect if I used this for Xamarin.Forms I would have gotten about the same advantages. The lesson from this is just because there is a cross platform version of the API, it doesn't mean it will perform well. Buyer beware.
Test 4: Querying SqLite Records
I wanted to test reading the 1,000 records I just wrote. I only did the 1,000 I originally wrote because I didn't want to deal with paging solutions for Cordova. A 10,000 record test performed very poorly when just creating a 10,000 for HTML table.
Java:
public ArrayList<String> getAllRecords() throws Exception {
if (dbConn == null) {
openConnection();
}
ArrayList<String> returnValue = new ArrayList<String>();
String sqlStatement = "SELECT * FROM " + TABLE_NAME;
Cursor cursor = dbConn.rawQuery(sqlStatement, null);
if (cursor.moveToFirst()) {
do {
returnValue.add(cursor.getString(1) + " " + cursor.getString(2));
} while (cursor.moveToNext());
}
return returnValue;
}
Objective-C:
- (NSMutableArray*)getAllRecords:(NSError**) error {
NSString *sqlStatement = NULL;
NSMutableArray *results;
sqlite3_stmt *sqlResult;
char *errInfo;
*error = nil;
if (dbConn == nil) {
[self openConnection:error];
return nil;
}
sqlStatement = @"SELECT * FROM testTable";
results = [[NSMutableArray alloc] init];
int result = sqlite3_exec(dbConn, [sqlStatement UTF8String], nil, nil, &errInfo);
result = sqlite3_prepare_v2(dbConn, [sqlStatement UTF8String], -1, &sqlResult, nil);
if (result == SQLITE_OK) {
while (sqlite3_step(sqlResult)==SQLITE_ROW) {
NSString* firstName = [NSString stringWithFormat:@"%s", (char*)sqlite3_column_text(sqlResult, 1)];
NSString* lastName = [NSString stringWithFormat:@"%s", (char*)sqlite3_column_text(sqlResult, 2)];
NSString* fullName = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
[results addObject:fullName];
}
sqlite3_finalize(sqlResult);
return results;
} else {
NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary];
[errorDetail setValue:@"Error loading records" forKey:NSLocalizedDescriptionKey];
*error = [NSError errorWithDomain:@"testDomain" code:101 userInfo:errorDetail];
return nil;
}
}
function showSqlRecords(sql) {
var db = window.sqlitePlugin.openDatabase({ name: "testDB.db" });
db.executeSql(sql, [], function (res) {
try {
var lstRecords = $("#lstRecords");
lstRecords.empty();
var lastValue = res.rows.length;
for (var i = 0; i < lastValue; i++) {
var listItem = "<li>" + res.rows.item(i).firstName + " " + res.rows.item(i).lastName + "</li>";
lstRecords.append(listItem);
}
} catch (err) {
alert(err.toString());
}
});
}
Xamarin (All Versions):
public IList<string> GetAllRecords()
{
if (dbConn == null)
{
OpenConnection();
}
var results = (from t in dbConn.Table<TestTable>() select t ).ToList();
var returnList = new List<string>();
foreach (var result in results)
{
returnList.Add(string.Format("{0} {1}", result.firstName, result.lastName));
}
return returnList;
}
Xamarin Android Alternate:
public IList<string> GetAllRecords()
{
if (dbConn == null) {
OpenConnection();
}
var returnValue = new List<String>();
var sqlStatement = "SELECT * FROM " + TABLE_NAME;
var results = dbConn.RawQuery(sqlStatement, null);
if (results.MoveToFirst()) {
do
{
returnValue.Add(results.GetString(1) + " " + results.GetString(2));
} while (results.MoveToNext());
}
return returnValue;
}
Development Platform | Test Avg. |
---|---|
Android | |
Java | 0.551 |
Cordova | 1.117 |
Classic Xamarin | 1.144 |
Xamarin.Forms | 0.89 |
Xamarin Classic Alternate | 0.601 |
iOS | |
Objective-C | 0.792 |
Cordova | 1.1745 |
Classic Xamarin | 0.799 |
Xamarin.Forms | 0.735 |
Reading records in Android was similarly bad for Xamarin on Android until I switched to the SQLiteOpenHelper implementation and then performance came online with the native example. As usual the interpreted JavaScript in Cordova slower but probably won't be a factor unless there is a large operation reading thousands of lines.
Test 5: Writing Lines to a File
I also wanted to test writing lines to a text file and how that performs. For each platform I write 1,000 lines to a file.
Java:
public void writeLineToFile(String line) throws Exception {
if (!textFile.exists()) {
this.createFile();
}
if (fileHandle == null) {
this.openFile();
}
fileHandle.write(line);
}
Objective-C:
- (void)writeLineToFile:(NSError**)error withTextToWrite:(NSData*)textToWrite {
*error = nil;
if (fileHandle == nil) {
[self openFile:error];
}
if (*error == nil) {
[fileHandle seekToEndOfFile];
[fileHandle writeData:textToWrite];
}
}
file.createWriter(function (fileWriter) {
fileWriter.seek(fileWriter.length);
var count = 0;
var line;
var message = "Writing line to file at index: ";
var maxLines = 999;
fileWriter.onwriteend = function(evt) {
count += 1;
if (count <= maxLines) {
line = message + count + "\\n";
fileWriter.write(line);
} else {
$.ui.unblockUI();
$.ui.popup({
title: "Success",
message: "All lines written to file.",
doneText: "OK",
cancelOnly: false
});
}
};
line = message + count + "\\n";
fileWriter.write(line);
public void WriteLineToFile(String line)
{
if (!File.Exists(filePath))
{
this.CreateFile();
}
if (streamWriter == null)
{
this.OpenFile();
}
streamWriter.WriteLine(line);
}
Development Platform | Test Avg. |
---|---|
Android | |
Java | 0.504 |
Cordova | 3.045 |
Classic Xamarin | 0.658 |
Xamarin.Forms | 0.715 |
iOS | |
Objective-C | 0.835 |
Cordova | 4.721 |
Classic Xamarin | 1.217 |
Xamarin.Forms | 1.17 |
When it comes to writing lines to a text file the vendor native technologies are the undisputed leaders in high performance. Xamain on iOS is close and a little slower on Android. Cordova comes in at 3-4 times slower than the other technologies. Some of this may come from the interpreted looping logic and not the plugin but the reality is that the overhead of the interpreted code that calls into the plug happens if you write one line or a thousand.
Test 6: Reading Lines from a File
Compliment to test 5, reading those 1,000 lines from a file and displaying them on a list.
Java:
public ArrayList<String> readFileContents() throws Exception {
BufferedReader reader = new BufferedReader(new FileReader(textFile));
String line;
ArrayList<String> returnValue = new ArrayList<String>();
while((line = reader.readLine()) != null){
returnValue.add(line);
}
reader.close();
return returnValue;
}
Objective-C:
- (NSArray*)readFileContents:(NSError**)error {
*error = nil;
if (fileHandle == nil) {
[self openFile:error];
}
if (*error == nil) {
[fileHandle seekToFileOffset: 0];
NSData* fileContents = [fileHandle readDataToEndOfFile];
NSString *fileString = [[NSString alloc] initWithData:fileContents encoding:NSUTF8StringEncoding];
return [fileString componentsSeparatedByString:@"\r\n"];
} else {
return nil;
}
}
file.file(function(innerFile) {
var reader = new FileReader();
reader.onerror = function(e) {
alert("Error");
}
reader.onloadend = function (e) {
var lines = e.target.result.split("\\n");
var lstFileLines = $("#lstFileLines");
lstFileLines.empty();
if (lines.length > 1) {
for (var i = 0; i < lines.length - 2; i++) {
var listItem = "<li>" + lines[i] + "</li>";
lstFileLines.append(listItem);
}
}
};
reader.readAsText(innerFile);
}, function(ex) {
$.ui.popup({
title: "Error",
message: "Error occurred opening file: " + ex.description,
doneText: "OK",
cancelOnly: false
});
Xamarin (All Versions):
public IList<string> ReadFileContents()
{
if (!File.Exists(filePath))
{
this.CreateFile();
}
var returnValue = new List<String>();
using (var streamReader = new StreamReader(filePath))
{
string line;
while ((line = streamReader.ReadLine()) != null)
{
returnValue.Add(line);
}
}
return returnValue;
}
Development Platform | Test Avg. |
---|---|
Android | |
Java | 0.438 |
Cordova | 1.126 |
Classic Xamarin | 0.596 |
Xamarin.Forms | 0.847 |
iOS | |
Objective-C | 0.727 |
Cordova | 1.16 |
Classic Xamarin | 0.706 |
Xamarin.Forms | 0.776 |
You want fast, go for native. Xamarin is as good or nearly as good except for Form on Android. This may have more to do with the performance of the list display than the actual reading of the list. That should be comparable to classic Xamarin. Same comments on Cordova as before, it's slower. If this is a factor or not depends a lot about the type of application.
I hope some of you find these comparisons helpful. The lessons I learned from this set of tests were:
- It is self-evident that not all implementations of code even on the same development platform perform the same. Having said that, be careful of wrappers around libraries that have unified APIs to allow for cross platform code. It may not perform as well as the specialized libraries for Android or iOS.
- While it is hard to differentiate slowness in code that comes from it being interpreted and what comes from the libraries they are using, to some extent it doesn't matter. In production apps the Cordova JavaScript code will be interpreted as it calls into plugins like SqLite so both will be a factor.
- The linking settings on Xamarin can have a huge difference in the size of the app. If you are using external libraries where you are not likely to use all the functionality use the Link All Assemblies option if possible.
Thanks everyone.
Source code for the tests can be found here: Performance Tests Source
Tuesday, December 23, 2014
Mobile Development Platform Performance (Native, Cordova, Classic Xamarin, Xamarin.Forms)
Last month I published my Mobile Technology Decision Making White Paper and I feel that goes a long way to helping choose what technology to use in the mobile space. One question I get asked frequently is how do the different mobile development platforms compare to each other from a performance perspective. While I've heard a lot of anecdotal information I have not seen a lot of formal comparisons. At Magenic we normally work with native (iOS, Android, WP), Cordova and Xamarin so I wanted to compare these. I plan on this being the first post on this topic.
For some background on testing methodology using Android and iOS.
The Development Platforms
- Native (Objective-C 64 bit and Java)
- Cordova (Multi-Device Hybrid Apps) using Intel's App Framework for the UI
- Classic Xamarin (64 bit unified beta for iOS)
- Xamarin.Forms (64 bit unified beta for iOS with beta version of Xamarin.Forms, note latest version of the unified API in the beta/alpha channels could not be used as it is not supported by Xamarin.Forms Note: 1.3.1 pre 1 was released Dec 24th so Xamarin.Forms may now work with the version of the unified iOS API in the alpha and beta channel)
The Devices
- iPad Mini (non Retina) running iOS 8.1.1 (12B435)
- ASUS K00F running Android 4.2.2
The Test Apps
Applications were made for each of the development platforms that are functionally similar. There was little (if no) effort to make them look exactly the same or even look "good". But they looked about the same. There were some differences such as Java, Classic Xamarin and Xamarin.Forms rendered the tabs on the top in Android as expected while the JavaScript library showed them on the bottom.
The Timing Methodology
Due to difficulties in knowing when things are "done", particularly with JavaScript, timings were handled via stopwatch. Each timing was taken ten times and the results were averaged. It should noted that hand timings have an accuracy of about 2/10 of a second so that does give us an approximate margin of error.
Test 1: Test App Size
The size of the application can impact how much bandwidth it takes to deploy and also have some impact on load times. For Android the size of the APKs was examined. For iOS I looked at Settings to find out how much space the apps took up on disk.
When it comes to application size Xamarin shows the extra size involved in the overhead of the .Net framework. There was an attempt to reduce the size of the deployed Xamarin application by using the "Link SDK assemblies only" setting. I am surprised in a very small application how large the difference is. However, from experience in "real" applications the difference is much less consequential as graphics and frameworks get added to the projects.
Test 2: Load Times
I wanted to see how long it took the application to load into memory. While the initial load time is important, many mobile applications tend to stay in memory so it tends to have a limited impact. For this test I made sure to close all applications before each timing.
In all cases the vendor native technologies loaded the fastest. Classic Xamarin loaded nearly as fast as the native languages. Xamarin.Forms and Cordova had the slowest load times. The Cordova load time on Android was particularly bad while on iOS the load times were close enough to not be a huge factor.
Test 3: Loading a List from Azure Mobile Services
In this test I wanted to look at getting data from an external service so I loaded 1000 records from Azure Mobile Services. For Xamarin iOS 64 bit I had to modify the Azure Mobile Services to be compatible with the unified API. The timings were taken from pushing the button to load the list until the results visibly came back and were displayed on a list on the screen.
Java:
Objective-C:
JavaScript:
Xamarin (All Versions):
Xamarin Classic Android Alternate:
*results in seconds
In many ways this test is showing how well the Azure Mobile Services libraries perform on the different platforms. Unsurprisingly Xamarin, with it's underpinnings of a .Net implementation, performs the best in this test. I was surprised to see the libraries for the native technologies perform the worst, both on Android and iOS.
I have heard that the Xamarin.Forms lists can perform poorly with large data sets. These results did not show that, at least with lists of up to 1000 records.
Test 4: Prime Number Calculation
In the final of my first series of tests I wanted to try out a CPU intensive operation. I created a Sieve of Eratosthenes on each of the platforms. My first plan was to calculate all prime numbers up to 50,000,000. This required some special handling of the method's array for both Objective-C and JavaScript. In the case of Objective-C I had to malloc memory to support arrays that large. Also for Objective-C and JavaScript I had to initialize the array items to 0. To keep the timings the same I did the array item initialization to 0 on all platforms even though it could have been left out for .Net (and Java I believe). It that was done, the .Net timings would have been even better.
I did end up having to settle for only calculating primes up to 5,000,000. The reason for this is that the JavaScript performed so poorly that I was unwilling to wait for it to complete 10 times.
Java:
Objective-C:
JavaScript:
Xamarin (All Versions):
*results in seconds
While I was expecting that JavaScript would be slower, I was unprepared for how much worse it was for this type of operation. This would make Cordova problematic for highly CPU bound work were performance is important (I wonder what that means for server side Node.js...). CPU bound work is not as important in many of today's mobile applications but given the history of increased performance for mobile CPUs and what happened in the PC market in the 1990's, it is likely that in the future CPU bound work will be more prevalent in mobile applications.
Having said that, using native HTML commands can be very fast. In the past I've testing loading JSON using HTML commands vs. the JSON.Net library on Xamarin and found them nearly comparable.
I was also surprised that Xamarin performed better than Objective-C by a noticeable amount. It also performed better than Java on Android but by a very marginal amount, well within the .2 second margin of error that manual timings give us.
That's it for my first installment of performance tests. Much of this code was made more difficult for Xamarin due to the flux around the 64 bit Unified iOS API. For next month I'll take a look at loading large JSON strings and perhaps something else.
I hope this is useful or you. If you have any ideas for performance tests I can perform, I'd love to hear them.
Source code for the tests can be found here: Performance Tests Source
For some background on testing methodology using Android and iOS.
The Development Platforms
- Native (Objective-C 64 bit and Java)
- Cordova (Multi-Device Hybrid Apps) using Intel's App Framework for the UI
- Classic Xamarin (64 bit unified beta for iOS)
- Xamarin.Forms (64 bit unified beta for iOS with beta version of Xamarin.Forms, note latest version of the unified API in the beta/alpha channels could not be used as it is not supported by Xamarin.Forms Note: 1.3.1 pre 1 was released Dec 24th so Xamarin.Forms may now work with the version of the unified iOS API in the alpha and beta channel)
The Devices
- iPad Mini (non Retina) running iOS 8.1.1 (12B435)
- ASUS K00F running Android 4.2.2
The Test Apps
Applications were made for each of the development platforms that are functionally similar. There was little (if no) effort to make them look exactly the same or even look "good". But they looked about the same. There were some differences such as Java, Classic Xamarin and Xamarin.Forms rendered the tabs on the top in Android as expected while the JavaScript library showed them on the bottom.
The Timing Methodology
Due to difficulties in knowing when things are "done", particularly with JavaScript, timings were handled via stopwatch. Each timing was taken ten times and the results were averaged. It should noted that hand timings have an accuracy of about 2/10 of a second so that does give us an approximate margin of error.
Test 1: Test App Size
The size of the application can impact how much bandwidth it takes to deploy and also have some impact on load times. For Android the size of the APKs was examined. For iOS I looked at Settings to find out how much space the apps took up on disk.
Development Platform | Size |
---|---|
Android | |
Java | 166kb |
Cordova | 433kb |
Classic Xamarin | 3.5mb |
Xamarin.Forms | 4.7mb |
iOS | |
Objective-C (64 bit) | 644kb |
Cordova | 2.7mb |
Classic Xamarin | 12.1mb |
Xamarin.Forms | 16.9mb |
When it comes to application size Xamarin shows the extra size involved in the overhead of the .Net framework. There was an attempt to reduce the size of the deployed Xamarin application by using the "Link SDK assemblies only" setting. I am surprised in a very small application how large the difference is. However, from experience in "real" applications the difference is much less consequential as graphics and frameworks get added to the projects.
Test 2: Load Times
I wanted to see how long it took the application to load into memory. While the initial load time is important, many mobile applications tend to stay in memory so it tends to have a limited impact. For this test I made sure to close all applications before each timing.
Development Platform | Test Avg. |
---|---|
Android | |
Java | 1.085 |
Cordova | 3.978 |
Classic Xamarin | 1.704 |
Xamarin.Forms | 2.764 |
iOS | |
Objective-C | 1.221 |
Cordova | 1.715 |
Classic Xamarin | 1.28 |
Xamarin.Forms | 1.813 |
In all cases the vendor native technologies loaded the fastest. Classic Xamarin loaded nearly as fast as the native languages. Xamarin.Forms and Cordova had the slowest load times. The Cordova load time on Android was particularly bad while on iOS the load times were close enough to not be a huge factor.
Test 3: Loading a List from Azure Mobile Services
In this test I wanted to look at getting data from an external service so I loaded 1000 records from Azure Mobile Services. For Xamarin iOS 64 bit I had to modify the Azure Mobile Services to be compatible with the unified API. The timings were taken from pushing the button to load the list until the results visibly came back and were displayed on a list on the screen.
Java:
public void addRecord(String firstName, String lastName, int index, String misc) throws Exception {
if (dbConn == null) {
openConnection();
}
ContentValues values = new ContentValues();
values.put("firstName", firstName);
values.put("lastName", lastName + index);
values.put("misc", misc);
dbConn.insertOrThrow(TABLE_NAME, null, values);
}
Objective-C:
- (void)addRecord:(NSString*)firstName withLastName:(NSString*)lastName withIndex:(int)index withMisc:(NSString*)misc withError:(NSError**)error {
NSString *sqlStatement = NULL;
char *errInfo;
*error = nil;
if (dbConn == nil) {
[self openConnection:error];
return;
}
sqlStatement = [NSString stringWithFormat:@"%@%@%@%@%d%@%@%@", @"INSERT INTO testTable (firstName, lastName, misc) VALUES ('", firstName, @"', '", lastName, index, @"', '", misc, @"')"];
int result = sqlite3_exec(dbConn, [sqlStatement UTF8String], nil, nil, &errInfo);
if (result != SQLITE_OK) {
NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary];
[errorDetail setValue:[NSString stringWithFormat:@"%@%s", @"Error writing record to database: ", errInfo] forKey:NSLocalizedDescriptionKey];
*error = [NSError errorWithDomain:@"testDomain" code:101 userInfo:errorDetail];
}
}
JavaScript:
db.executeSql("INSERT INTO testTable (firstName, lastName, misc) VALUES (?,?,?)", ["test", lastName, "12345678901234567890123456789012345678901234567890"], function (res) {
successCount++;
if (successCount === maxValue) {
$.ui.popup({
title: "Success",
message: "All records written to database",
doneText: "OK",
cancelOnly: false
});
$.ui.unblockUI();
}
}, function (e) {
$.ui.popup({
title: "Error",
message: "An error has occurred adding records: " + e.toString(),
doneText: "OK",
cancelOnly: false
});
$.ui.unblockUI();
return;
});
Xamarin (All Versions):
public void AddRecord(string fName, string lName, int i, string m)
{
if (dbConn == null)
{
OpenConnection();
}
var testRecord = new TestTable {firstName = fName, id = 0, lastName = lName + i, misc = m};
dbConn.Insert(testRecord);
}
Xamarin Classic Android Alternate:
public void AddRecord(string firstName, string lastName, int index, string misc)
{
if (dbConn == null)
{
OpenConnection();
}
ContentValues values = new ContentValues();
values.Put("firstName", firstName);
values.Put("lastName", lastName + index);
values.Put("misc", misc);
dbConn.InsertOrThrow(TABLE_NAME, null, values);
}
Development Platform | Test 1 | Test 2 | Test 3 | Test 4 | Test 5 | Test 6 | Test 7 | Test 8 | Test 9 | Test 10 | Test Avg. |
---|---|---|---|---|---|---|---|---|---|---|---|
Android | |||||||||||
Java | 22.71 | 17.5 | 18.04 | 17.7 | 18.63 | 20.33 | 2.68 | 2.42 | 2.16 | 2.34 | 2.369 |
Cordova | 25.99 | 24.76 | 27.05 | 23.3 | 24.06 | 22.86 | 2.12 | 2.02 | 1.94 | 2.48 | 2.149 |
Classic Xamarin | 34.07 | 27.38 | 32.05 | 38.77 | 29.27 | 34.63 | 1.61 | 1.63 | 1.84 | 1.85 | 1.738 |
Xamarin.Forms | 1.99 | 1.76 | 2.32 | 1.91 | 1.9 | 1.58 | 1.93 | 2.02 | 2.03 | 1.64 | 1.908 |
iOS | |||||||||||
Objective-C | 2.38 | 2.44 | 2.24 | 2.3 | 2.34 | 2.32 | 2.32 | 2.35 | 2.2 | 2.27 | 2.316 |
Cordova | 3.57 | 2.18 | 2.07 | 1.95 | 1.97 | 2.05 | 2.04 | 1.93 | 2.2 | 1.96 | 2.192 |
Classic Xamarin | 2 | 1.87 | 1.88 | 2.06 | 1.74 | 1.9 | 1.81 | 1.94 | 1.75 | 1.96 | 1.891 |
Xamarin.Forms | 2.11 | 2.01 | 2.23 | 1.96 | 1.95 | 2.07 | 2.12 | 2.16 | 2.08 | 2.1 | 2.079 |
In many ways this test is showing how well the Azure Mobile Services libraries perform on the different platforms. Unsurprisingly Xamarin, with it's underpinnings of a .Net implementation, performs the best in this test. I was surprised to see the libraries for the native technologies perform the worst, both on Android and iOS.
I have heard that the Xamarin.Forms lists can perform poorly with large data sets. These results did not show that, at least with lists of up to 1000 records.
Test 4: Prime Number Calculation
In the final of my first series of tests I wanted to try out a CPU intensive operation. I created a Sieve of Eratosthenes on each of the platforms. My first plan was to calculate all prime numbers up to 50,000,000. This required some special handling of the method's array for both Objective-C and JavaScript. In the case of Objective-C I had to malloc memory to support arrays that large. Also for Objective-C and JavaScript I had to initialize the array items to 0. To keep the timings the same I did the array item initialization to 0 on all platforms even though it could have been left out for .Net (and Java I believe). It that was done, the .Net timings would have been even better.
I did end up having to settle for only calculating primes up to 5,000,000. The reason for this is that the JavaScript performed so poorly that I was unwilling to wait for it to complete 10 times.
Java:
private int getPrimesFromSieve(int maxValue)
{
byte[] primes = new byte[maxValue + 1];
for (int i = 0; i <=maxValue; i++)
{
primes[i] = 0;
}
int largestPrimeFound = 1;
for (int i = 2; i <=maxValue; i++)
{
if (primes[i - 1] == 0)
{
primes[i - 1] = 1;
largestPrimeFound = i;
}
int c = 2;
int mul = i*c;
for (; mul <= maxValue;)
{
primes[mul - 1] = 1;
c++;
mul = i*c;
}
}
return largestPrimeFound;
}
Objective-C:
- (int) getPrimesFromSieve: (int) maxValue {
Byte *primes;
primes = (Byte *) malloc(maxValue * sizeof(Byte));
for (int i=1; i<=maxValue; i++)
{
primes[i-1] = 0;
}
int largestPrimeFound;
largestPrimeFound = 1;
for (int i=2; i<=maxValue; i++)
{
if(primes[i-1] == 0)
{
primes[i-1] = 1;
largestPrimeFound = i;
}
int c=2;
int mul = i*c;
for(; mul <= maxValue;)
{
primes[mul-1] = 1;
c++;
mul = i*c;
}
}
return largestPrimeFound;
}
JavaScript:
function getPrimesFromSieve(maxValue) {
var primes = new Uint8Array(new ArrayBuffer(Number(maxValue)));
for (var i = 0; i <=maxValue; i++) {
primes[i] = 0;
}
var largestPrimeFound = 1;
for (i = 2; i <= maxValue; i++) {
if (primes[i - 1] == 0) {
primes[i - 1] = 1;
largestPrimeFound = i;
}
var c = 2;
var mul = i * c;
for (; mul <= maxValue;) {
primes[mul - 1] = 1;
c++;
mul = i * c;
}
}
return largestPrimeFound;
}
Xamarin (All Versions):
public static int GetPrimesFromSieve(int maxValue)
{
var primes = new byte[maxValue + 1];
for (var i = 0; i <=maxValue; i++)
{
primes[i] = 0;
}
var largestPrimeFound = 1;
for (var i = 2; i <=maxValue; i++)
{
if (primes[i - 1] == 0)
{
primes[i - 1] = 1;
largestPrimeFound = i;
}
var c = 2;
var mul = i*c;
for (; mul <= maxValue;)
{
primes[mul - 1] = 1;
c++;
mul = i*c;
}
}
return largestPrimeFound;
}
Development Platform | Test 1 | Test 2 | Test 3 | Test 4 | Test 5 | Test 6 | Test 7 | Test 8 | Test 9 | Test 10 | Test Avg. |
---|---|---|---|---|---|---|---|---|---|---|---|
Android | |||||||||||
Java | 4.31 | 4.31 | 4.2 | 4.33 | 4.39 | 4.37 | 4.32 | 4.45 | 4.34 | 4.4 | 4.342 |
Cordova | 91.69 | 95 | 94.31 | 94.4 | 94.73 | 94.1 | 94.1 | 91.8 | 93.63 | 97.75 | 94.151 |
Classic Xamarin | 4.27 | 4.25 | 4.15 | 4.32 | 4.51 | 4.41 | 4.22 | 4.12 | 4.14 | 4.19 | 4.258 |
Xamarin.Forms | 4.21 | 4.17 | 4.31 | 4.3 | 4.2 | 4.34 | 4.29 | 4.36 | 4.22 | 4.19 | 4.259 |
iOS | |||||||||||
Objective-C | 5.04 | 5.49 | 5.38 | 4.86 | 4.8 | 5.02 | 5.03 | 4.83 | 4.84 | 4.85 | 5.014 |
Cordova | 66.96 | 67.36 | 67.22 | 67.3 | 67.17 | 67.44 | 67.13 | 67.11 | 67.58 | 67.64 | 67.291 |
Classic Xamarin | 4.41 | 4.42 | 4.35 | 4.34 | 4.49 | 4.37 | 4.17 | 4.27 | 4.39 | 4.28 | 4.349 |
Xamarin.Forms | 4.51 | 4.33 | 4.31 | 4.31 | 4.33 | 4.4 | 4.41 | 4.4 | 4.33 | 4.46 | 4.379 |
While I was expecting that JavaScript would be slower, I was unprepared for how much worse it was for this type of operation. This would make Cordova problematic for highly CPU bound work were performance is important (I wonder what that means for server side Node.js...). CPU bound work is not as important in many of today's mobile applications but given the history of increased performance for mobile CPUs and what happened in the PC market in the 1990's, it is likely that in the future CPU bound work will be more prevalent in mobile applications.
Having said that, using native HTML commands can be very fast. In the past I've testing loading JSON using HTML commands vs. the JSON.Net library on Xamarin and found them nearly comparable.
I was also surprised that Xamarin performed better than Objective-C by a noticeable amount. It also performed better than Java on Android but by a very marginal amount, well within the .2 second margin of error that manual timings give us.
That's it for my first installment of performance tests. Much of this code was made more difficult for Xamarin due to the flux around the 64 bit Unified iOS API. For next month I'll take a look at loading large JSON strings and perhaps something else.
I hope this is useful or you. If you have any ideas for performance tests I can perform, I'd love to hear them.
Source code for the tests can be found here: Performance Tests Source
Thursday, October 30, 2014
Xamarin.Forms Circle Images
It is a common pattern in mobile applications to display an image in the shape of a circle. Xamarin.Forms does not have any functionality for this right out of the box but the Xamarin.Forms team showed us how this can be done at Evolve 2014. That implementation can be found here:
Xamarin.Forms Team Circle Image
This implementation by +James Montemagno is good for making an exact circle with a white border. It uses masking and it's really fast. One thing it doesn't do is handle what happens when the height and width of the control are different or what happens when you set the Aspect property. I'm ignoring the catch(Exception ex) statements James, but just don't let +Jason Bock see them! (not that my code is perfect either, I try to do better each and every day.)
So here is an example of the renderers when using with controls that have a different height and width with the Aspect property set:
OK, we can see that the Android and Windows Phone implementations in this scenario are about the same. They always draw a round image though the two implementations show a slightly different portion of the picture. It probably with have displayed fine had the control's height and width been the same. These are generally behaving as would be expected under the Aspect Fit setting.
The iOS implementation is doing something very different. It is behaving as though the Aspect property were set to Fill in all scenarios. The upshot of the deal is that in most cases the programmer is going to set the RequestedHeight and RequestedWidth the same and these renderers are going to be great (and fast). But how about those other cases, what would we expect to see then?
This is what you would expect to see. Aspect Fill fills the entire control and what portion of the circle image is outside of that area is cropped off. The top and bottom in this case. The fill setting takes the circle and spreads it out to fill the entire control area resulting in an oval. Aspect Fit makes a perfect circle in the center of the control. Since the control area is wider than it is tall a letterbox situation is created with blank space on either side of the circle.
Note: My Windows Phone implementation is now yet complete. I'm currently using a renderer in the style that +James Montemagno created.
The following implementation is now in the open source XLabs library for Xamarin.Forms. It can be found here XForms Labs. As of the time of this writing it has not yet made it to the nuget package.
The first thing I did was create a custom view that derived from the standard Image view as so:
Ok, I know what you may be thinking... Where's the beef? As it turns out at the present time I don't need to change anything intrinsic about the view itself, just how it is rendered. To do this I subclass from the Image view so I can create a custom renderer for that without impacting base Image view.
To use this in Xaml it is as simple as:
Other than calling it a CircleImage and referencing it the line "xmlns:controls="clr-namespace:Xamarin.Forms.Labs.Controls;assembly=Xamarin.Forms.Labs"" it is just like working with a standard Image view.
So let's take a look at what I did with iOS to make a custom renderer:
The header is exactly what you would expect. The new renderer for the CircleImage is registered as part of the namespace and the custom renderer derives from the ImageRenderer.
One of the things that I need to think about is how will I accomplish the Aspect property working. This requires considering a few things, do I need to manipulate the image, if I do how do I know the image is loaded, ensure there are no memory leaks and finally use masking as the Xamarin.Forms implementation when possible because it performs better.
So right of the bat I want to do masking for Fill because that's what works by default. First we'll look at the OnElementChanged method:
This isn't normally where I would look for manipulating images as this is where the base class's ImageRenderer will start loading them asynchronously from the source. I can however set a mask, which is what I do if the Aspect is Fill.
Where the real interesting code comes in is during OnElementPropertyChanged. For my masking I want to know that the control is drawing and that means that the height and width have changed. For the bitmap manipulation I need to know when the image is loaded from the source. Luckily the Xamarin.Forms team sets an IsLoading property to true when the image starts loading and turns it to false when complete. That means when the IsLoading property changes to false I know when the load operation is complete.
NOTE: On Windows Phone changes to the IsLoading property are not raised to the renderer's OnElementPropertyChanged method as they are in iOS and Android. I suspect this is an oversight but for now other methods need to be used.
The code here is pretty simple. Base on the right phase in the lifecycle and what Aspect we are using we either draw for our masking (DrawFill) or re-size the image (DrawOther). For now I'll ignore the masking and focus on the DrawOther method.
A couple of things to notice. I look at the aspect to figure out the size of the image I need. If it is Aspect Fill I expect that the image may be higher or wider than the control's requested width or height. For Aspect Fit I expect that neither the height nor width of the image will be larger than the requested amounts but that at least one will be equal. In both cases I expect the image to be square.
With the new width and height I now scale the image to the right size (image.Scale(new SizeF(width, height));). I can then use the FromRoundedRect function to crop the image and make it round with the rest transparent. When I am done I make the new image the image for the control's image property and that's what will be used.
Android was a little more complex but in many ways the same. The first thing was that the masking works correctly with the Aspect Fit scenario instead of Fill like on iOS. The second is that we want to apply the mask on the DrawChild method override and not OnElementPropertyChanged.
I check to see if this is actually an Aspect Fit situation and if not just don't so anything. If it is I want to clip the existing image. The AddCircle command works nicely for this. This command always takes a center X and Y and a radius to draw a circle. We calculate how large we want the circle to be by looking at the width and height of the control as they have been calculated by this point. Once this is done successfully we return true and have a nice round image view that fits nicely in the control's bounds.
For the other two we look at OnElementPropertyChanged like we did on iOS and if we are done loading convert the drawable into a bitmap.
Once we have a new source bitmap I call ReshapeImage to manipulate it:
Android works primarily in rectangles for the area to draw in. I get two rectangles, a scaled rectangle that is used for the area to resize the image and a target rectangle for where it will display on the control. A new target bitmap is created that will be used by the control. I won't go too deeply into the image manipulation commands but one thing to notice is the DrawRoundRect has an x radius and a y radius. That allows me to draw a nice oval in fill mode as I was not able to do in iOS.
The other thing that may look strange is setting the Element's WidthRequest property to itself. I did this because I wanted to call the base class's InvalidateMeasure method to let the control take appropriate action with the new image. Since this is a protected method I can't call it directly. However, when the WidthRequest is set, InvalidateMeasure is then called. It was just a way to force an InvalidateMeasure call.
I hope you find this useful. The full code case be found in the XForms labs project and will shortly be coming to our nuget packages. What's next? Finishing the Windows Phone renderer and then adding a border, allowing the user to set the color and width.
I'll be speaking in a few weeks at Modern Apps Live in Orlando (strangely enough not on Xamarin). I hope to see you there. Modern Apps Live 2014 Orlando
Xamarin.Forms Team Circle Image
This implementation by +James Montemagno is good for making an exact circle with a white border. It uses masking and it's really fast. One thing it doesn't do is handle what happens when the height and width of the control are different or what happens when you set the Aspect property. I'm ignoring the catch(Exception ex) statements James, but just don't let +Jason Bock see them! (not that my code is perfect either, I try to do better each and every day.)
So here is an example of the renderers when using with controls that have a different height and width with the Aspect property set:
OK, we can see that the Android and Windows Phone implementations in this scenario are about the same. They always draw a round image though the two implementations show a slightly different portion of the picture. It probably with have displayed fine had the control's height and width been the same. These are generally behaving as would be expected under the Aspect Fit setting.
The iOS implementation is doing something very different. It is behaving as though the Aspect property were set to Fill in all scenarios. The upshot of the deal is that in most cases the programmer is going to set the RequestedHeight and RequestedWidth the same and these renderers are going to be great (and fast). But how about those other cases, what would we expect to see then?
Note: My Windows Phone implementation is now yet complete. I'm currently using a renderer in the style that +James Montemagno created.
The following implementation is now in the open source XLabs library for Xamarin.Forms. It can be found here XForms Labs. As of the time of this writing it has not yet made it to the nuget package.
The first thing I did was create a custom view that derived from the standard Image view as so:
public class CircleImage : Image
{
}
Ok, I know what you may be thinking... Where's the beef? As it turns out at the present time I don't need to change anything intrinsic about the view itself, just how it is rendered. To do this I subclass from the Image view so I can create a custom renderer for that without impacting base Image view.
To use this in Xaml it is as simple as:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Xamarin.Forms.Labs.Controls;assembly=Xamarin.Forms.Labs"
x:Class="Xamarin.Forms.Labs.Sample.Pages.Controls.CircleImagePage">
<controls:CircleImage HeightRequest="75" WidthRequest="100" HorizontalOptions="Fill" Aspect="AspectFill">
<controls:CircleImage.Source>
<OnPlatform x:TypeArguments="ImageSource">
<OnPlatform.iOS>
<FileImageSource File="panic.jpg" />
</OnPlatform.iOS>
<OnPlatform.Android>
<FileImageSource File="panic.jpg" />
</OnPlatform.Android>
<OnPlatform.WinPhone>
<FileImageSource File="Images/panic.jpg" />
</OnPlatform.WinPhone>
</OnPlatform>
</controls:CircleImage.Source>
</controls:CircleImage>
</ContentPage>
Other than calling it a CircleImage and referencing it the line "xmlns:controls="clr-namespace:Xamarin.Forms.Labs.Controls;assembly=Xamarin.Forms.Labs"" it is just like working with a standard Image view.
So let's take a look at what I did with iOS to make a custom renderer:
[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
namespace Xamarin.Forms.Labs.iOS.Controls.CircleImage
{
public class CircleImageRenderer : ImageRenderer
The header is exactly what you would expect. The new renderer for the CircleImage is registered as part of the namespace and the custom renderer derives from the ImageRenderer.
One of the things that I need to think about is how will I accomplish the Aspect property working. This requires considering a few things, do I need to manipulate the image, if I do how do I know the image is loaded, ensure there are no memory leaks and finally use masking as the Xamarin.Forms implementation when possible because it performs better.
So right of the bat I want to do masking for Fill because that's what works by default. First we'll look at the OnElementChanged method:
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
if (Control == null || e.OldElement != null || Element == null || this.Element.Aspect != Aspect.Fill )
return;
var min = Math.Min(Element.Width, Element.Height);
Control.Layer.CornerRadius = (float)(min / 2.0);
Control.Layer.MasksToBounds = false;
Control.ClipsToBounds = true;
}
This isn't normally where I would look for manipulating images as this is where the base class's ImageRenderer will start loading them asynchronously from the source. I can however set a mask, which is what I do if the Aspect is Fill.
Where the real interesting code comes in is during OnElementPropertyChanged. For my masking I want to know that the control is drawing and that means that the height and width have changed. For the bitmap manipulation I need to know when the image is loaded from the source. Luckily the Xamarin.Forms team sets an IsLoading property to true when the image starts loading and turns it to false when complete. That means when the IsLoading property changes to false I know when the load operation is complete.
NOTE: On Windows Phone changes to the IsLoading property are not raised to the renderer's OnElementPropertyChanged method as they are in iOS and Android. I suspect this is an oversight but for now other methods need to be used.
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Control == null) return;
if (this.Element.Aspect == Aspect.Fill)
{
if (e.PropertyName == VisualElement.HeightProperty.PropertyName ||
e.PropertyName == VisualElement.WidthProperty.PropertyName)
{
DrawFill();
}
}
else
{
if (e.PropertyName == Image.IsLoadingProperty.PropertyName
&& !this.Element.IsLoading && this.Control.Image != null)
{
DrawOther();
}
}
}
The code here is pretty simple. Base on the right phase in the lifecycle and what Aspect we are using we either draw for our masking (DrawFill) or re-size the image (DrawOther). For now I'll ignore the masking and focus on the DrawOther method.
private void DrawOther()
{
int height = 0;
int width = 0;
int top = 0;
int left = 0;
switch (this.Element.Aspect)
{
case Aspect.AspectFill:
height = (int)this.Control.Image.Size.Height;
width = (int)this.Control.Image.Size.Width;
height = this.MakeSquare(height, ref width);
left = (((int)this.Control.Image.Size.Width - width) / 2);
top = (((int)this.Control.Image.Size.Height - height) / 2);
break;
case Aspect.AspectFit:
height = (int)this.Control.Image.Size.Height;
width = (int)this.Control.Image.Size.Width;
height = this.MakeSquare(height, ref width);
left = (((int)this.Control.Image.Size.Width - width) / 2);
top = (((int)this.Control.Image.Size.Height - height) / 2);
break;
default:
throw new NotImplementedException();
}
UIImage image = this.Control.Image;
var clipRect = new RectangleF(0, 0, width, height);
var scaled = image.Scale(new SizeF(width, height));
UIGraphics.BeginImageContextWithOptions(new SizeF(width, height), false, 0f);
UIBezierPath.FromRoundedRect(clipRect, Math.Max(width, height) / 2).AddClip();
scaled.Draw(new RectangleF(0, 0, scaled.Size.Width, scaled.Size.Height));
UIImage final = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
this.Control.Image = final;
}
A couple of things to notice. I look at the aspect to figure out the size of the image I need. If it is Aspect Fill I expect that the image may be higher or wider than the control's requested width or height. For Aspect Fit I expect that neither the height nor width of the image will be larger than the requested amounts but that at least one will be equal. In both cases I expect the image to be square.
With the new width and height I now scale the image to the right size (image.Scale(new SizeF(width, height));). I can then use the FromRoundedRect function to crop the image and make it round with the rest transparent. When I am done I make the new image the image for the control's image property and that's what will be used.
Android was a little more complex but in many ways the same. The first thing was that the masking works correctly with the Aspect Fit scenario instead of Fill like on iOS. The second is that we want to apply the mask on the DrawChild method override and not OnElementPropertyChanged.
protected override bool DrawChild(Canvas canvas, global::Android.Views.View child, long drawingTime)
{
if (this.Element.Aspect == Aspect.AspectFit)
{
var radius = Math.Min(Width, Height)/2;
var strokeWidth = 10;
radius -= strokeWidth/2;
var path = new Path();
path.AddCircle(Width/2, Height/2, radius, Path.Direction.Ccw);
canvas.Save();
canvas.ClipPath(path);
var result = base.DrawChild(canvas, child, drawingTime);
path.Dispose();
return result;
}
return base.DrawChild(canvas, child, drawingTime);
}
I check to see if this is actually an Aspect Fit situation and if not just don't so anything. If it is I want to clip the existing image. The AddCircle command works nicely for this. This command always takes a center X and Y and a radius to draw a circle. We calculate how large we want the circle to be by looking at the width and height of the control as they have been calculated by this point. Once this is done successfully we return true and have a nice round image view that fits nicely in the control's bounds.
For the other two we look at OnElementPropertyChanged like we did on iOS and if we are done loading convert the drawable into a bitmap.
protected async override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Image.IsLoadingProperty.PropertyName && !this.Element.IsLoading
&& this.Control.Drawable != null)
{
//Should only be true right after an image is loaded
if (this.Element.Aspect != Aspect.AspectFit)
{
using (var sourceBitmap = Bitmap.CreateBitmap(this.Control.Drawable.IntrinsicWidth, this.Control.Drawable.IntrinsicHeight, Bitmap.Config.Argb8888))
{
var canvas = new Canvas(sourceBitmap);
this.Control.Drawable.SetBounds(0, 0, canvas.Width, canvas.Height);
this.Control.Drawable.Draw(canvas);
this.ReshapeImage(sourceBitmap);
}
}
}
}
Once we have a new source bitmap I call ReshapeImage to manipulate it:
private void ReshapeImage(Bitmap sourceBitmap)
{
if (sourceBitmap != null)
{
var sourceRect = GetScaledRect(sourceBitmap.Height, sourceBitmap.Width);
var rect = this.GetTargetRect(sourceBitmap.Height, sourceBitmap.Width);
using (var output = Bitmap.CreateBitmap(rect.Width(), rect.Height(), Bitmap.Config.Argb8888))
{
var canvas = new Canvas(output);
var paint = new Paint();
var rectF = new RectF(rect);
var roundRx = rect.Width() / 2;
var roundRy = rect.Height() / 2;
paint.AntiAlias = true;
canvas.DrawARGB(0, 0, 0, 0);
paint.Color = Android.Graphics.Color.ParseColor("#ff424242");
canvas.DrawRoundRect(rectF, roundRx, roundRy, paint);
paint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.SrcIn));
canvas.DrawBitmap(sourceBitmap, sourceRect, rect, paint);
this.Control.SetImageBitmap(output);
// Forces the internal method of InvalidateMeasure to be called.
this.Element.WidthRequest = this.Element.WidthRequest;
}
}
}
Android works primarily in rectangles for the area to draw in. I get two rectangles, a scaled rectangle that is used for the area to resize the image and a target rectangle for where it will display on the control. A new target bitmap is created that will be used by the control. I won't go too deeply into the image manipulation commands but one thing to notice is the DrawRoundRect has an x radius and a y radius. That allows me to draw a nice oval in fill mode as I was not able to do in iOS.
The other thing that may look strange is setting the Element's WidthRequest property to itself. I did this because I wanted to call the base class's InvalidateMeasure method to let the control take appropriate action with the new image. Since this is a protected method I can't call it directly. However, when the WidthRequest is set, InvalidateMeasure is then called. It was just a way to force an InvalidateMeasure call.
I hope you find this useful. The full code case be found in the XForms labs project and will shortly be coming to our nuget packages. What's next? Finishing the Windows Phone renderer and then adding a border, allowing the user to set the color and width.
I'll be speaking in a few weeks at Modern Apps Live in Orlando (strangely enough not on Xamarin). I hope to see you there. Modern Apps Live 2014 Orlando
Monday, July 7, 2014
Xamarin.Forms.Labs Version 1.1.0 Released
For the last several weeks several developers from around the globe have been working on a project to extend the functionality given in Xamarin.Forms to deliver more cross platform capabilities for Android, iOS and Windows Phone 8. This project is called Xamarin.Froms.Labs. Today we have released version 1.1.0 and it is now in Nuget for your use.
One of the largest improvements is in the structure of the packages themselves. All platform specific packages are being depreciated in favor of single packages that install correctly for all supported platforms.
The 1.1.0 version includes the following package structure for use with Android, iOS and Windows Phone:
Xamarin.Forms.Labs - Core package containing PCL and platform specific dlls including custom controls, renderers and services.
Xamarin.Forms.Labs.Services.Autofac - Includes PCL with extensions to use with Autofac.
Xamarin.Forms.Labs.Services.Ninject - Includes PCL with extensions to use with Ninject.
Xamarin.Forms.Labs.Services.SimpleContainer - Includes PCL with extensions to use with Simple Injector.
Xamarin.Forms.Labs.Services.TinyIOC- Includes PCL and platform specific DLLs with extensions to use with TinyIOC
Xamarin.Forms.Labs.Services.Serialization.JsonNET - Includes PCL to use JsonNet with Xamarin.Forms.
Xamarin.Forms.Labs.Services.Serialization.ProtoBuf - Includes PCL to use ProtoBuf with Xamarin.Forms.
Xamarin.Forms.Labs.Cryptography - Includes PCL to give cross platform cryptography with Xamarin.Forms.
Xamarin.Forms.Labs.Caching.SQLiteNet - Includes PCL to enable cross platform caching using SQLite with Xamarin.Forms.
The following packages are being depreciated or not part of 1.1.0, primarily because of their platform specific nature:
Xamarin.Forms.Labs.Droid
Xamarin.Forms.Labs.iOS
Xamarin.Forms.Labs.WP
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3.Droid
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3.iOS
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3.WP8
Xamarin.Forms.Labs.Services.TinyIOC.Droid
Xamarin.Forms.Labs.Services.TinyIOC.iOS
Xamarin.Forms.Labs.Services.TinyIOC.WP8
Information about the contents and features of Xamarin.Forms.Labs can be found on our wiki here:
Xamarin.Forms.Labs Wiki
The following bugs/features are part of 1.1.0:
-Changed structure of Nuget packages to remove platform specific packages.
-Corrected references in Nuget packages.
-XML documentation and source added to Nuget packages.
-ImageButton with no size specified no longer throws an exception.
-WebImage control added with Android and iOS implementations.
-Fixed Json serializer so default settings are no longer null.
-Changed ImageButton to use the standard Xamarin.Forms Source property for the image to display instead of a string Image property. The Image property was removed as a result. This is a breaking change.
- Fixed issue with HybridWebView callback not being called.
- Corrected issue where ViewFactory did not support mutliple Views with the same view model.
-Renderer support for Windows Phone added for the ExtendedLabel, ExtendedTextCell and the ExtendedViewCell.
- Renderer support for Android added for the ExtendedViewCell.
- Platform specific font properties on the ExtendedLabel are depreciated.
- RelayCommand depreciated.
If you would like to know more about the Xamarin.Forms.Labs team, would like to contribute functionality, comment or enter a bug you can always go to our repository on GitHub: Xamarin-Forms-Labs.
Thanks and happy coding!
One of the largest improvements is in the structure of the packages themselves. All platform specific packages are being depreciated in favor of single packages that install correctly for all supported platforms.
The 1.1.0 version includes the following package structure for use with Android, iOS and Windows Phone:
Xamarin.Forms.Labs - Core package containing PCL and platform specific dlls including custom controls, renderers and services.
Xamarin.Forms.Labs.Services.Autofac - Includes PCL with extensions to use with Autofac.
Xamarin.Forms.Labs.Services.Ninject - Includes PCL with extensions to use with Ninject.
Xamarin.Forms.Labs.Services.SimpleContainer - Includes PCL with extensions to use with Simple Injector.
Xamarin.Forms.Labs.Services.TinyIOC- Includes PCL and platform specific DLLs with extensions to use with TinyIOC
Xamarin.Forms.Labs.Services.Serialization.JsonNET - Includes PCL to use JsonNet with Xamarin.Forms.
Xamarin.Forms.Labs.Services.Serialization.ProtoBuf - Includes PCL to use ProtoBuf with Xamarin.Forms.
Xamarin.Forms.Labs.Cryptography - Includes PCL to give cross platform cryptography with Xamarin.Forms.
Xamarin.Forms.Labs.Caching.SQLiteNet - Includes PCL to enable cross platform caching using SQLite with Xamarin.Forms.
The following packages are being depreciated or not part of 1.1.0, primarily because of their platform specific nature:
Xamarin.Forms.Labs.Droid
Xamarin.Forms.Labs.iOS
Xamarin.Forms.Labs.WP
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3.Droid
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3.iOS
Xamarin.Forms.Labs.Services.Serialization.ServiceStackV3.WP8
Xamarin.Forms.Labs.Services.TinyIOC.Droid
Xamarin.Forms.Labs.Services.TinyIOC.iOS
Xamarin.Forms.Labs.Services.TinyIOC.WP8
Information about the contents and features of Xamarin.Forms.Labs can be found on our wiki here:
Xamarin.Forms.Labs Wiki
The following bugs/features are part of 1.1.0:
-Changed structure of Nuget packages to remove platform specific packages.
-Corrected references in Nuget packages.
-XML documentation and source added to Nuget packages.
-ImageButton with no size specified no longer throws an exception.
-WebImage control added with Android and iOS implementations.
-Fixed Json serializer so default settings are no longer null.
-Changed ImageButton to use the standard Xamarin.Forms Source property for the image to display instead of a string Image property. The Image property was removed as a result. This is a breaking change.
- Fixed issue with HybridWebView callback not being called.
- Corrected issue where ViewFactory did not support mutliple Views with the same view model.
-Renderer support for Windows Phone added for the ExtendedLabel, ExtendedTextCell and the ExtendedViewCell.
- Renderer support for Android added for the ExtendedViewCell.
- Platform specific font properties on the ExtendedLabel are depreciated.
- RelayCommand depreciated.
If you would like to know more about the Xamarin.Forms.Labs team, would like to contribute functionality, comment or enter a bug you can always go to our repository on GitHub: Xamarin-Forms-Labs.
Thanks and happy coding!
Subscribe to:
Posts (Atom)