Xamarin Shared Projects (SAPs) vs Xamarin Portable Class Libraries (PCLs). Which to Use?

Overview

One of the advantages of using Xamarin over the traditional platform-specific approach is the possibility of sharing a significant portion of your code across iOS, Android, and Windows platform. This article will explore two ways to structure shared code, which is Shared Projects (SAPs) and Portable Class Libraries (PCLs). We will look at the pros and cons of each approach and ways of providing access to platform-specific features in each model.

The first thing to remember is; every Xamarin project will have a small portion of platform-specific code. According to iCircuit, every Xamarin project will have 15% up to 30% of platform-specific code. It usually includes:

  • UI definition
  • Connection logic
  • Platform-specific features

The goal of using Xamarin is to maximize code sharing for mobile features which is not dependent on any specific platform. Shared code includes:

  • Web service access code
  • Data parsing operation
  • Database connection
  • Business process logic

 So let's see how we structure Xamarin projects to able to efficiently share code.

Shared Projects (SAPs)

Shared Projects or sometimes referred to as Shared Application Projects or Shared Assets Project defines a specific project type where the intent is to actually share all the files in that project with other projects. By referencing platform specific project to shared project, all of the files and assets included in the shared project will be copied and included in the binary compilation process of the targeted platform. 

Shared project does not produce an assembly or an executable. It requires platform specific project to be built thus cannot be compiled independently.

One advantage to Shared Projects is that you can utilize platform-specific APIs directly. This is possible because the source file is actually compiled as part of the project. However, the file is included in the other projects. As a result, we need some way to isolate platform-specific code so that we don't compile it on the other platforms.

Platform-Specific Code Isolation Methods

  1. Conditional Compilation
    Conditional compilation uses compiler directives as a way to tell the compiler which block of code that needs to be compiled. Predefined symbol #if and #elif along with its platform name can be used to distinguish which block of code to be compiled for the specific platform build.

    public static string Databasefile {
       get {
            var filename = "HRdb.db3";
    #if WINDOWS_PHONE || WINDOWS_UWP
            var path = filename;
    #elif __ANDROID__
            var path = Path.Combine(Environment.GetFolderPath(
                Environment.SpecialFolder.Personal, filename
            ));
    #elif __IOS__
            var docPath = Environment.GetFolderPath(
            Environment.SpecialFolder.Personal
            );
            var path = Path.Combine(docPath, "..", "Library", filename);
    #endif
            return path;
        }
    }


    The downside of this approach is the maintainability. As the solution gets more complex, it will be difficult to see which part of the code is getting compiled and to verify which code changes won't break the build for other platforms without compiling each platform in turn.

  2. Class Mirroring
    Class Mirroring is where we use classes in a shared project to define a particular functionality which is defined differently for each target platform.

    Alert.cs in Android Platform Project.
    internal class Alert 
    {
        internal static void Show(string title, string message)
        {
            new AlertDialog.Builder(Application.Context)
                .SetTitle(title)
                .SetMessage(message);
        }
    }

    Alert.cs
    in iOS Platform Project.
    internal class Alert
    {
        internal static void Show(string title, string message)
        {
            new UIAlertView(title, message, null, "OK")
                .Show();
        }
    }

    Notice that the class is marked as internal. This works because the shared project code is compiled as your platform-specific target that is included with. You also need to make sure the public signature are the same for any method used or a compile-time error will occur.

    This approach is more maintainable that the conditional compilation in the term of code clarity. However, it can still cause build issues when you are not always building every platform together.

  3. Partial Classes + Methods
    Partial class is when we split the definition of a single class across multiple source files. The key to this approach is that you can have a portion of a class that can be defined in the shared project and the other that can be defined in the platform specific project.

    NoteManager.cs in the Shared Project
    partial class NoteManager
    {
        void OnDeleteNote()
        {
            if (ShowAlert("Warning!", "..."))
            {
                // Insert what to do after showing alert on each platform
            }
        }
    }

    NoteManager.cs
    in Android Platform Project
    partial class NoteManager
    {
        bool ShowAlert(string title, string msg)
        {
            // Insert Android show alert implementation here
        }
    }

    NoteManager.cs
    in iOS Platform Project
    partial class NoteManager
    {
        bool ShowAlert(string title, string msg)
        {
            // Insert iOS show alert implementation here
        }
    }

    You can mark a method as partial to tell the compiler that a particular method may not always be defined on every platform (optional).

Portable Class Libraries (PCLs)

A Portable Class Library is a class library which is capable of running on multiple .NET platforms (desktop, Xamarin.iOS, Xamarin.Android, Windows Phone, etc.). They provide a more structured container for sharing code but also require more architecture and thought when using platform-specific features.

Portable Class Library provide a way for you to create a portable assembly that can target multiple platforms. A PCL project will create a binary DLL that not directly tied to the runtime of a platform but instead retargeted based on the platform that they end up executing on. This model of project will not share code on the source level. Instead, it will share binary component that can be run on multiple .NET platforms including Windows Desktop (Win32, WPF, WinForms), WinRT (Store Application), Silverlight, and of course Xamarin.

The features available to PCL is defined by something called a profile. A PCL profile defines a combination of platforms the PCL can be executed on. Based on the selected profile the compiler will restrict the available API service to be the intersection of API available on those supported platforms.

Basically, the more platform you choose to support the more limited your API selection will be. Choose only the frameworks that are needed.

Working with Platform-Specific Features in PCL

  1. Delegates and Events
    One way to work on platform-specific features is to expose delegates and events in the PCL types, and then provide some basic behavior in the platform specific assembly.  This allows unique platform specific code to be called by the shared platform independent code. 

    Dialer.cs in PCL Project
    public class Dialer {
        public static Func<string, bool> MakeCallImpl;
    
        public bool MakeCall(string number)
        {
            if (MakeCallImpl(number))
            {
                // Implement what to do after calling.
            }
        }
    }

    A method in iOS Platform Project calling MakeCallImpl delegate function
    Dialer.MakeCallImpl = number =>
    {
        return UIApplication.SharedApplication
            .OpenUrl(new NSUrl("tel:" + number));
    }

    Platform-specific projects always have access to all the things that are in the PCL. And so for the iOS project we can supply a delegate that then does the work of dialing the phone. So that way, when the PCL invokes the MakeCallImpl delegate we end up in the platform specific project where we can do whatever we need to accomplish the feature.

  2. Dependency Injection
    Dependency Injection or sometimes is known as Inversion of Control (IOC), is a design principle in which code creating a new object supplies the other objects that the new object depends on for operation.  In simpler terms, an abstraction is used to connect between the general functionality in the PCL and the functionality implementation in the platform-specific project. Below is the example of cross-platform code to load song files from the file system. 

    IStreamLoader.cs in the PCL project. This interface abstracts away the file stream loading process.
    public interface IStreamLoader
    {
        System.IO.Stream GetStreamForFilename(string filename);
    }
    

    StreamLoader.cs
    implements the IStreamLoader.cs interface for the iOS platform project.
    class StreamLoader : IStreamLoader
    {
        public Stream GetStreamForFilename(string filename)
        {
            return File.OpenRead(filename);
        }
    }

    Static class SongLoader.cs prepares to inject the abstraction
    public static class SongLoader
    {
        const string Filename = "songs.json";
        ...
        public static IStreamLoader Loader { get; set; }
    
        private static Stream OpenData()
        {
            if (Loader == null)
                throw new Exception("Must set platform Loader before calling Load.");
    
            return Loader.GetStreamForFilename(Filename);
        }
    }

    Method Load() in SongLoader.cs injects the abstraction, allowing its class to use it.
    // MyTunesViewController.cs
    public async override void ViewDidLoad()
    {
       base.ViewDidLoad();
    
       // Load the data
       SongLoader.Loader = new StreamLoader();
       var data = await SongLoader.Load();
       ...
    }
  3. Dependency Service
    Dependency Service uses DependencyService Data Annotation to provide platform-specific functionality. DependencyService is a dependency resolver. In practice, an interface is defined and finds DependencyService the correct implementation of that interface from the various platform projects. It's quite similar to Dependency Injection method, but we will let DependencyService finds the right implementation rather than manually injects it. Below is the example of text-to-speech implementation in Xamarin using DependencyService.

    ITextToSpeech.cs in the PCL project. This interface abstracts away the text-to-speech translation process.
    public interface ITextToSpeech {
        void Speak ( string text ); //note that interface members are public by default
    }

    TextToSpeechImplementation.cs
     in Windows Phone platform project implements ITextToSpeech interface for Windows Phone Platform. Each implementation of the interface needs to be registered with DependencyService with a DataAnnotation attribute. The following code registers the implementation for Windows Phone.
    [assembly: Xamarin.Forms.Dependency (typeof (TextToSpeechImplementation))]
    namespace TextToSpeech.WinPhone
    {
      public class TextToSpeechImplementation : ITextToSpeech
      {
          public TextToSpeechImplementation() {}
    
          public async void Speak(string text)
          {
              SpeechSynthesizer synth = new SpeechSynthesizer();
              await synth.SpeakTextAsync(text);
          }
      }
    }

    Once the project has been set up with a common interface and implementations for each platform, call DependencyService in the PCL project to get the right implementation at runtime.
    DependencyService.Get<ITextToSpeech>().Speak("Hello from Xamarin Forms");

Pros and Cons

Between Shared Project and PCL, none of them is completely better our outperform the other. Each model has its own advantages and disadvantages.

Shared Projects

(+) Allows you to share code across multiple projects.
(+) Shared code can relatively easy be branched based on the platform using compiler directives.
(+) Application project can include platform-specific references that the share code can utilize.
(-) A shared project does not have output assembly.
(-) Refactoring that affect code inside 'inactive' compiler directive will not update the code. 

Portable Class Libraries

(+) Allows you to share code across multiple projects.
(+) Refactoring operations always update all affected references.
(-) Cannot use compiler directives. Accessing platform specific features in PCL is more difficult and requires architectural thinking.
(-) Only a subset of the .NET Framework is available to use, determined by the selected profile.

Which to Use and When?

Shared Projects is relatively easy to use than PCL. Shared project is a good solution for application developers who writes code application only for themselves or their company they work for and not intent to distribute it to other developers.

Though Portable Class Libraries is more difficult to use, it is the elegant way to share code across multiple platforms. It maintains code separation and lowly coupled classes. It is a good solution if you plan to share the resulting assembly with other developers. I personally prefer and recommend you to use PCL for any real-world projects.

References

  1. Xamarin Documentation
  2. Xamarin Self-Guided Learning - Introduction to Cross-Platform Mobile Development
  3. MSDN - Cross-Platform Development with the Portable Class Library