left-icon

Xamarin.Forms for macOS Succinctly®
by Alessandro Del Sole

Previous
Chapter

of
A
A
A

CHAPTER 8

Accessing Platform-Specific APIs

Accessing Platform-Specific APIs


So far, you have seen what Xamarin.Forms offers in terms of features that are available on each supported platform, walking through pages, layouts, and controls that expose properties and capabilities that will certainly run on Android and iOS. Though this simplifies cross-platform development, it is not enough to build real-world mobile applications. In fact, more often than not, mobile apps need to access sensors, the file system, the camera, and the network; to send push notifications; and more. Each operating system manages these features with native APIs that cannot be shared across platforms and, therefore, Xamarin.Forms cannot map into cross-platform objects.

However, if Xamarin.Forms did not provide a way to access native APIs, it would not be very useful. Luckily enough, Xamarin.Forms provides multiple ways to access platform-specific APIs that you can use to access practically everything of each platform. Thus, there is no limit to what you can do with Xamarin.Forms. In order to access platform features, you will need to write C# code in each platform project. This is what this chapter explains, together with all the options you have to access iOS and Android APIs from your shared codebase.

The Device class and the OnPlatform method

The Xamarin.Forms namespace exposes an important class called Device. This class allows you to detect the platform your app is running on, and the device idiom (tablet, phone, desktop). This class is particularly useful when you need to adjust the user interface based on the platform. The following code demonstrates how to take advantage of the Device.RuntimePlatform property to detect the running platform and make UI-related decisions based on its value:

// Label1 is a Label view in the UI

switch(Device.RuntimePlatform)

{

    case Device.iOS:

        Label1.FontSize = Device.GetNamedSize(NamedSize.Large, Label1);

        break;

    case Device.Android:

        Label1.FontSize = Device.GetNamedSize(NamedSize.Medium, Label1);

        break;

    case Device.WinPhone:

        Label1.FontSize = Device.GetNamedSize(NamedSize.Medium, Label1);

        break;

    case Device.Windows:

        Label1.FontSize = Device.GetNamedSize(NamedSize.Large, Label1);

        break;

}

RuntimePlatform is of type string and can be easily compared against specific constants called iOS, Android, WinPhone, and Windows that represent, with self-explanatory names, the supported platforms. Obviously, the two latter constants will only work with Visual Studio 2017 on Windows devices. The GetNamedSize method automatically resolves the Default, Micro, Small, Medium, and Large platform font size and returns the corresponding double, which avoids the need to supply numeric values that would be different for each platform. The Device.Idiom property allows you to determine if the current device the app is running on is a phone, tablet, or desktop PC, and returns one of the values from the TargetIdiom enumeration:

switch(Device.Idiom)

{

    case TargetIdiom.Desktop:

        // Desktop PCs

        break;

    case TargetIdiom.Phone:

        // Phones

        break;

    case TargetIdiom.Tablet:

        // Tablets

        break;

    case TargetIdiom.Unsupported:

        // Unsupported devices

        break;

}

You can also decide how to adjust UI elements based on the platform and idiom in XAML. Code Listing 27 demonstrates how to adjust the Padding property of a page, based on the platform.

Code Listing 27

<?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:views="clr-namespace:App1.Views"

             x:Class="App1.Views.MainPage">

    <ContentPage.Padding>

        <OnPlatform x:TypeArguments="Thickness"

                iOS="0, 20, 0, 0"

                Android="0, 10, 0, 0"

                WinPhone="0, 10, 0, 0" />

    </ContentPage.Padding>

</ContentPage>

With the OnPlatform tag, you can specify a different property value based on the iOS, Android, and WinPhone platforms (the latter is only supported on Windows). The property value depends on the x:TypeArguments attribute, which represents the .NET type for the property, Thickness in this particular case. Similarly, you can work with OnIdiom and the TargetIdiom enumeration in XAML.

Tip: In iOS, it is best practice to set a page padding of 20 from the top, like in the previous snippet. If you do not do this, your page will overlap the system bar.

Working with the dependency service

Most of the time, mobile apps need to offer interaction with the device hardware, sensors, system apps, and file system. Accessing these features from shared code is not possible, because their APIs have unique implementations on each platform. However, Xamarin.Forms provides a simple solution to this problem that relies on the service locator pattern: in the shared project, you write an interface that defines the required functionalities, then inside each platform project, you write classes that implement the interface through native APIs. Finally, you use the DependencyService class and its Get method to retrieve the proper implementation based on the platform your app is running on.

For example, suppose your app needs to work with SQLite local databases. Assuming you have installed the SQLite-Net-Pcl NuGet package in your solution, in the PCL project, you can write the following sample interface called IDatabaseConnection that defines the signature of a method that must return the database path:

public interface IDatabaseConnection

{

    SQLite.SQLiteConnection DbConnection();

}

Tip: A complete walkthrough of using local SQLite databases in Xamarin.Forms is available on MSDN Magazine from the author of this e-book.

At this point, in each platform project, you need to provide an implementation of this interface, because file names, path names, and, more generally, the file system are platform-specific. Add a new class file called DatabaseConnection.cs to the iOS and Android projects. Code Listing 28 provides the iOS implementation, and Code Listing 29 provides the Android implementation.

Code Listing 28

using System;

using SQLite;

using System.IO;

using App1.iOS;

 

[assembly: Xamarin.Forms.Dependency(typeof(DatabaseConnection))]

namespace App1.iOS

{

    public class DatabaseConnection : IDatabaseConnection

    {

        public SQLiteConnection DbConnection()

        {

            string dbName = "MyDatabase.db3";

            string personalFolder =

              System.Environment.

              GetFolderPath(Environment.SpecialFolder.Personal);

            string libraryFolder =

              Path.Combine(personalFolder, "..""Library");

            string path = Path.Combine(libraryFolder, dbName);

            return new SQLiteConnection(path);

        }

    }

}

Code Listing 29

using Xamarin.Forms;

using App1.Droid;

using SQLite;

using System.IO;

 

[assemblyDependency(typeof(DatabaseConnection))]

namespace App1.Droid

{

    public class DatabaseConnectionIDatabaseConnection

    {

        public SQLiteConnection DbConnection()

        {

            string dbName = "MyDatabase.db3";

            string path = Path.Combine(System.Environment.

              GetFolderPath(System.Environment.

              SpecialFolder.Personal), dbName);

            return new SQLiteConnection(path);

        }

    }

}

Common to each platform-specific implementation is decorating the namespace with the Dependency attribute, assigned at the assembly level, which uniquely identifies the implementation of the IDatabaseConnection interface at runtime. In the DbConnection method body, you can see how each platform leverages its own APIs to work with filenames. In the PCL project, you can simply resolve the proper implementation of the IDatabaseConnection interface as follows:

// Get the connection to the database

SQLiteConnection

database = DependencyService.Get<IDatabaseConnection>().DbConnection();

The DependencyService.Get generic method receives the interface as the type parameter and resolves the implementation of that interface according to the current platform. With this approach, you do not need to worry about determining the current platform and invoking the corresponding native implementations, since the dependency service does the job for you. This approach applies to all native APIs you need to invoke, and provides the most powerful option to access platform-specific features in Xamarin.Forms.

Working with plugins

When accessing native APIs, most of the time your actual need is to access features that exist cross-platform, but with APIs that are totally different from one another. For example, iOS and Android devices both have a camera, they both have a GPS sensor that returns the current location, and so on. For scenarios in which you need to work with capabilities that exist cross-platform, you can leverage plugins. These are libraries that consist of an abstract implementation of native APIs that provide capabilities available cross-platform. They also avoid the need to use the dependency service and write platform-specific code in a large number of situations. Plugins are free and open source, and are available as NuGet packages. An updated list of available plugins is available on GitHub.

Among others, some popular plugins are the Connectivity plugin (which makes it easy to handle network connectivity), the Media plugin (which makes it simple to capture pictures and videos from the PCL project), and the Geolocator plugin (which provides an abstraction to access geolocation).

For example, suppose you want to detect if a network connection is available before accessing the Internet in your app. You can use the NuGet Package Manager to download and install the Connectivity plugin shown in Figure 42. For each plugin, there is a link to the documentation page on GitHub, which I certainly recommend you visit when you use any plugins.

Installing plugins

Figure 42: Installing plugins

Make sure the plugin is installed into all the projects in the solution. I will not go into plugins’ architecture here; I will only explain how to use them. If you are interested in their architecture, you can read this blog post from the Xamarin team.

As a general rule, the root namespace of a plugin exposes a singleton class that exposes the requested feature. For example, the root Plugin.Connectivity namespace exposes the CrossConnectivity class, whose Current property represents the singleton instance that you can use as follows in your shared code (and without the need to work with platform projects):

if(CrossConnectivity.Current.IsConnected)

{

    // Connection is available

}

 

CrossConnectivity.Current.ConnectivityChanged += 

    ((sender, e)=> 

        {

            // Connection status changed

        });

Among others, this class exposes the IsConnected property, which returns true if a network connection is available, and the ConnectivityChanged event, which is raised when the connection changes. The class also exposes the IsRemoteReachable method you can use to check whether a remote site is reachable, and the Bandwidths property, which returns a collection of available bandwidths (not supported on iOS). By convention, the name of each singleton class exposed by plugins begins with Cross.

As you can see in the previous snippet, you have a cross-platform abstraction that you use in the PCL that does not require complex, platform-specific implementations calling native APIs manually. Plugins can save a huge amount of time, but, of course, they can provide a cross-platform interface only for those features that are commonly available. For example, the Connectivity Plugin exposes networking features that are common to iOS, Android, and Windows, but not native features that cannot be exposed with a cross-platform abstraction, and would instead require working with native APIs directly. However, I strongly recommend you check to see if a plugin exists when you need to access native features not included in Xamarin.Forms out of the box. In fact, in most cases, you will need common features, and plugins will help you save time and keep your code simpler to maintain.

Tip: Another example of plugins is provided in the next chapter, when we discuss the app lifecycle.

Working with native views

In previous sections, you looked at how to interact with native Android and iOS features by accessing their APIs directly in C# code or through plugins. In this section, you will instead see how to use native views in Xamarin.Forms, which is extremely useful when you need to extend views provided by Xamarin.Forms, or when you wish to use native views that Xamarin.Forms does not wrap into shared objects out of the box.

Embedding native views in XAML

Xamarin.Forms allows you to add native views directly into the XAML markup. This feature is a recent addition available since version 2.3.3, and it makes it really easy to use native visual elements. To understand how working with native views in XAML works, consider Code Listing 30.

Code Listing 30

<?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:ios="clr-namespace:UIKit;

                        assembly=Xamarin.iOS;targetPlatform=iOS"

             xmlns:androidWidget="clr-namespace:Android.Widget;

                        assembly=Mono.Android;targetPlatform=Android"

             xmlns:formsandroid="clr-namespace:Xamarin.Forms;

                                 assembly=Xamarin.Forms.Platform.Android;

                                 targetPlatform=Android"

             x:Class="App1.MainPage" Title="Native views">

    <ContentPage.Content>

        <StackLayout>

            <ios:UILabel Text="Native Text" View.HorizontalOptions="Start"/>

            <androidWidget:TextView Text="Native Text" 

                   x:Arguments="{x:Static formsandroid:Forms.Context}" />

        </StackLayout>

    </ContentPage.Content>

</ContentPage>

In the XAML of the root page, you first need to add XML namespaces that point to the namespaces of native platforms. The formsandroid namespace is required by Android widgets to get the current UI context. Remember that you can choose a different name for the namespace identifier. Using native views is then really simple, since you just need to declare the specific view for each platform you want to target.

In Code Listing 30, the XAML markup includes a UILabel native label on iOS, and a TextView native label on Android. With Android views, you must supply the current Xamarin.Forms UI context, which is done with a special syntax that binds the static (x:Static) Forms.Context property to the view. You can interact with views in C# code as you would normally do, such as with event handlers, but the very good news is that you can also assign native properties to each view directly in your XAML.

Working with custom renderers

Put succinctly, renderers are classes that Xamarin.Forms uses to access and render native views and that bind the Xamarin.Forms’s views and layouts discussed in Chapters 4 and 5 to their native counterparts. For example, the Label view discussed in Chapter 4 maps to a LabelRenderer class that Xamarin.Forms uses to render the native UILabel and TextView views on iOS and Android, respectively. Xamarin.Forms’s views completely depend on renderers to expose their look and behavior.

The good news is that you can override the default renderers with the so-called custom renderers, which you can use to extend or override features in the Xamarin.Forms views. A custom renderer is therefore a class that inherits from the renderer that maps the native view, and is the place where you can change the layout, override members, and change the view’s behavior.

An example will be helpful to understand custom renderers more. Suppose you want an Entry view to auto-select its content when the user taps the text box. Xamarin.Forms has no support for this scenario, so you can create a custom renderer that works at the platform level. In the PCL project, add a new class called AutoSelectEntry that looks like the following:

using Xamarin.Forms;

namespace App1

{

    public class AutoSelectEntry: Entry

    {

    }

}

The reason for creating a class that inherits from Entry is that, otherwise, the custom renderer you will create shortly would be applied to all the Entry views in your user interface. By creating a derived view, you can decide to apply the custom renderer only to this one. If you instead want to apply the custom renderer to all the views in the user interface of that type, you can skip this step. The next step is creating a class that inherits from the built-in renderer (the EntryRenderer in this case), and provides an implementation inside each platform project.

Note: In the next code examples, you will find many native objects and members. I will only highlight those that are strictly necessary to your understanding. The descriptions for all the others can be found in the Xamarin.iOS and Xamarin.Android documentation.

Code Listing 31 shows how to implement a custom renderer in iOS, and Code Listing 32 shows the Android version.

Code Listing 31

[assemblyExportRenderer(typeof(AutoSelectEntry), 

           typeof(AutoSelectEntryRenderer))]

namespace App1.iOS

{

    public class AutoSelectEntryRendererEntryRenderer

    {

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)

        {

            base.OnElementChanged(e);

            var nativeTextField = Control;

            nativeTextField.EditingDidBegin += (object sender, EventArgs eIos) =>

            {

                nativeTextField.PerformSelector(new ObjCRuntime

                               .Selector("selectAll"),

                null, 0.0f);

            };

        }

    }

}

Code Listing 32

using Xamarin.Forms;

using Xamarin.Forms.Platform.Android;

using NativeAccess;

using NativeAccess.Droid;

 

[assemblyExportRenderer(typeof(AutoSelectEntry), 

 typeof(AutoSelectEntryRenderer))]

namespace App1.Droid

{

    public class AutoSelectEntryRendererEntryRenderer

    {

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)

        {

            base.OnElementChanged(e);

            if (e.OldElement == null)

            {

                var nativeEditText = (global::Android.Widget.EditText)Control;

                nativeEditText.SetSelectAllOnFocus(true);

            }

        }

    }

}

In each platform implementation, you override the OnElementChanged method to get the instance of the native view via the Control property, and then you invoke the code necessary to select all the text box content using native APIs.

It is necessary to mention the ExportRenderer attribute at the assembly level that tells Xamarin.Forms to render views of the specified type (AutoSelectEntry in this case) with an object of type AutoSelectEntryRenderer, instead of the built-in EntryRenderer. Once you have the custom renderer ready, you can use the custom view in XAML as you would normally do, as demonstrated in Code Listing 33.

Code Listing 33

<?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:local="clr-namespace:App1"

             Title="Main page"

             x:Class="App1.MainPage">

 

    <StackLayout Orientation="Vertical" Padding="20">

        <Label Text="Enter some text:"/>

 

        <local:AutoSelectEntry x:Name="MyEntry" Text="Enter text..." 

         HorizontalOptions="FillAndExpand"/>

    </StackLayout>

</ContentPage>

Tip: The local XML namespace is declared by default, so adding your view is even simpler. Additionally, IntelliSense will show your custom view in the list of available objects from that namespace.

If you now run this code, you will see that the text in the AutoSelectEntry view will be automatically selected when the text box is tapped. Custom renderers are very powerful because they allow you to completely override the look and behavior of any views. However, sometimes you just need some minor customizations that can instead be provided through effects.

Hints for effects

Effects are a recent addition to the Xamarin.Forms toolbox and can be thought of as simplified custom renderers, limited to changing some layout properties without changing the behavior of a view. An effect is made of two classes: a class that inherits from PlatformEffect and must be implemented in all the platform projects, and a class that inherits from RoutingEffect and resides in the PCL (or shared) project, whose responsibility is resolving the platform-specific implementation of the custom effect.

You handle the OnAttached and OnDetached events to provide the logic for your effect. Because their structure is similar to custom renderers’ structures, I will not cover effects in more detail here, but it is important that you know they exist. You can check out the official documentation, which explains how to consume built-in effects and create custom ones.

Chapter summary

Mobile apps often need to work with features that you can only access through native APIs. Xamarin.Forms provides access to the entire set of native APIs on iOS and Android via a number of possible options. With the Device class, you can get information on the current system from your shared code. With the DependencyService class and its Get method, you can resolve cross-platform abstractions of platform-specific code in your PCL.

With plugins, you have ready-to-use cross-platform abstractions for the most common scenarios, such as (but not limited to) accessing the camera, network information, settings, or battery status. In terms of native visual elements, you can embed iOS and Android native views directly in your XAML. You can also write custom renderers or effects to change the look and feel of your views. Actually, each platform also manages the app lifecycle with its own APIs. Fortunately, Xamarin.Forms has a cross-platform abstraction that makes it simpler, as explained in the next chapter.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.