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.

SharedProject

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!

 

Advertisements