Xamarin saves a lot of programming, testing and maintenance effort by using a shared codebase. You write code, find bugs and fix them, just Once, not the number of platforms that you are supporting. Although we do have some platform specific code like the native UI code or the controller code that takes you from UI controls to the logic or platform specific features, there is still about 70-85% code that can be shared.
There is a C# version of every native library type in Xamarin which lets you program in C# and it generates a full native code for the respective platform. You, thus, get native performance from shared code.
You can share code for accessing web services or databases, parsing data or your core business logic. For a feature that requires platform specific code, you can create abstractions over it and call it from the shared code, but before that, it is a good idea to check if an abstraction already exists
- .NET Foundation
- NuGet
- Xamarin Component Store (commercial binary components)
- github.com/Xamarin/plugins (Open Source Plugins)
- github.com/Xamarin
- Xamarin.Social
- Xamarin.Auth (OAuth)
- Xamarin.Mobile (common mobile services)
These abstraction are really helpful an saves lot of your time.
So, how is shared code organized?
Shared Projects
Xamarin has a new type of project called Shared Projects; Common code lives in this container. While compiling, all files in shared project are compiled into each target platform. Be sure to add reference to shared project in each of the platform project that you have.
There are a few techniques of writing shared code that we should now look into.
Conditional Compilation
We can create blocks of code with #if compiler directives (same as C language). For each platform we can define these conditinal compilation symbols through project build settings.
The down side is that it is difficult to see what get compiled and if a change breaks another target build without compiling for it. It is also difficult to manage code like this.
// Shared Project public class NotesApp { void SaveFile() { #if __IOS__ new UIAlertView("File Save", "File saved to cloud drive", null, "OK").Show(); #endif } }
Some of the known symbols are
Symbol | What it represents |
---|---|
#if __MOBILE__ |
Any mobile project (vs. desktop) |
#if __ANDROID__ |
Xamarin.Android – defined by the compiler |
#if __IOS__ |
Xamarin.iOS – defined by the compiler |
#if __TVOS__ |
iOS tvOS – defined by the compiler |
#if __WATCHOS__ |
iOS watchOS – defined by the compiler |
#if WINDOWS_UWP |
Windows 10 UWP – defined in build settings |
Class Mirroring
This is a more structured approach in which we define a class in each of the platforms but we keep the class name and methods same. We can then use the same class name and methods in the shared code and its implementation will come from its respective target project.
The class can use inheritance to provide shared functionality where possible and maximize shared code.
This is easier to maintain but you still need to compile for all platforms to be sure that there are no breaking changes.
// Shared Project public class NotesApp { void SaveFile() { Message.Show("File Save", "File saved to cloud drive"); } } // iOS Project internal class Message() { internal static void Show(string head, string msg) { new UIAlertView(head, message, null, "OK").Show(); } } // Android Project internal class Message() { internal static void Show(string head, string msg) { Android.App.AlertDialog.Builder dialog = new AlertDialog.Builder(this); AlertDialog alert = dialog.Create(); alert.SetTitle(head); alert.SetMessage("msg"); alert.SetButton("OK", (c, ev) => { // 'OK' click action }); alert.Show(); } }
Partial Classes and Methods
C# has Partial keywork to define partial classes and methods. A partial class definition can be split across multiple source files. The common part of class is defined in the shared project while platform specific methods are added by respective projects in the class.
A partial method on the other hand is to tell the compiler that the method is optional. At compile time if the method definition is found in respective project, code is emited to call the method. Otherwise every call to the method is removed from the binary.
This is helpful if you want to do something on a particular platform and not on other.
// Shared Project partial class NotesApp { partial void Show(string head, string msg); void OnSaveFile() { Show("File Save", "File saved to cloud drive"); } } // iOS Project partial class NotesApp() { void Show(string head, string msg) { new UIAlertView(head, message, null, "OK").Show(); } } // Android Project partial class NotesApp() { // Method not defined }
These patterns will help you write shared code and make it easy to maintain and extend.
Try it yourself!
// //Conditional Compilation Sample // // Sample Shared Code public class Phone { public string Name { get; set; } public long Number { get; set; } public string Address { get; set; } } public static class PhoneDirectory { const string YelloPages = "YellowPages.json"; public static async Task<IEnumerable<Phone>> Load() { using (var reader = new StreamReader(await OpenFile())) { return JsonConvert.DeserializeObject<List<Phone>>(await reader.ReadToEndAsync()); } } private async static Task<Stream> OpenFile() { #if __ANDROID__ return Android.App.Application.Context.Assets.Open(YelloPages); #endif #if __IOS__ return File.OpenRead(YelloPages); #endif #if WINDOWS_UWP var fileStream = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(YelloPages); return await fileStream.OpenStreamForReadAsync(); #endif return null; } } // Sample iOS code public override async void ViewDidLoad() { base.ViewDidLoad(); var data = await PhoneDirectory.Load(); TableView.Source = new ViewControllerSource<Phone>(TableView) { DataSource = data.ToList(), TextProc = s => s.Name, DetailTextProc = s => s.Number + "-" + s.Address, }; } // Sample Android code protected override async void OnCreate(Bundle bundle) { base.OnCreate(bundle); var data = await PhoneDirectory.Load(); ListAdapter = new ListAdapter<Phone>() { DataSource = data.ToList(), TextProc = s => s.Name, DetailTextProc = s => s.Number + "-" + s.Address }; } // Sample UWP Code protected override async void OnNavigatedTo(NavigationEventArgs e) { DataContext = await PhoneDirectory.Load(); } // // * if you are wondering what is DataContext?, // DataContext is the default source of bindings in WPF // We are populating that in our code and UI refreshes on its own //
Happy Hacking!