CHAPTER 9
Until now, 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, iOS, and Windows. 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; send push notifications; and more. Each operating system manages these features with native APIs that cannot be shared across platforms and, therefore, that Xamarin.Forms cannot map into cross-platform objects.
If Xamarin.Forms did not provide a way to access native APIs, it would not be very useful. Luckily, Xamarin.Forms provides multiple ways to access platform-specific APIs that you can use to access practically everything from 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, Android, and Windows APIs from your shared codebase.
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— iOS, Android, WinPhone, and Windows—that represent the supported platforms.
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 (UWP only), and returns one of the values from the TargetIdiom enumeration:
switch(Device.Idiom)
{
case TargetIdiom.Desktop:
// UWP desktop
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 32 demonstrates how to adjust the Padding property of a page, based on the platform.
Code Listing 32
<?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 property value depends on the x:TypeArguments attribute, which represents the .NET type for the property, Thickness in this particular case. Similarly, you can also 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 don’t do this, your page will overlap the system bar. Also, for iPhone X and higher models, you also need to take care of the safe area. For more info, take a look at my article on how to handle the safe area in Xamarin.Forms.
The Device class is useful not only for fine-tuning the user interface according to the device, but also for other features, such as localization. For example, the Device class exposes the FlowDirection property that makes it easier to implement right-to-left localization, and whose value can be bound to the FlowDirection property of each view, like in the following example:
<ContentPage FlowDirection="{x:Static Device.FlowDirection}">
The ContentPage’s content will be displayed according to the localization information retrieved from the device. In addition to XAML, you can also work with this property in C# code.
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.
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-standard NuGet package in your solution, in the .NET Standard project, you can write the following sample interface called IDatabaseConnection, which 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 ebook.
At this point, you need to provide an implementation of this interface in each platform project, 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, Android, and Windows projects.
Code Listing 33 provides the iOS implementation, Code Listing 34 provides the Android implementation, and Code Listing 35 provides the Windows implementation.
Code Listing 33
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 34
using Xamarin.Forms; using App1.Droid; using SQLite; using System.IO;
[assembly: Dependency(typeof(DatabaseConnection))] namespace App1.Droid { public class DatabaseConnection: IDatabaseConnection { public SQLiteConnection DbConnection() { string dbName = "MyDatabase.db3"; string path = Path.Combine(System.Environment. GetFolderPath(System.Environment. SpecialFolder.Personal), dbName); return new SQLiteConnection(path); } } } |
Code Listing 35
using SQLite; using Xamarin.Forms; using System.IO; using Windows.Storage; using App1.UWP;
[assembly: Dependency(typeof(DatabaseConnection))] namespace App1.UWP { public class DatabaseConnection : IDatabaseConnection { public SQLiteConnection DbConnection() { { string dbName = "MyDatabase.db3"; string path = Path.Combine(ApplicationData. Current.LocalFolder.Path, dbName); return new SQLiteConnection(path); } } } } |
Each implementation decorates 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 .NET Standard 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.
Tip: All the code examples described in this chapter require a using Xamarin.Essentials directive.
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, Android, and Windows devices all have a camera, they all 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 a library called Xamarin.Essentials, a free and open-source library that is automatically added to Xamarin.Forms solutions at creation. At the time of writing, the latest stable version available is 1.6.1. This library covers almost 40 features that exist cross-platform, such as sensors, location, network connection, communication, and much more. I will provide some examples next to help you understand how and where it can be so useful.
Tip: On Android, you need to enable the ACCESS_NETWORK_STATE permission in the project manifest.
One of the most common requirements in mobile apps is checking for the availability of a network connection. With Xamarin.Essentials, this is very easy:
if(Connectivity.NetworkAccess == NetworkAccess.Internet)
{
// Internet is available
}
The Connectivity class provides everything you need to detect network connection and state. The NetworkAccess property returns the type of connection, with values from the NetworkAccess enumeration: Internet, ConstrainedInternet, Local, None, and Unknown. The ConnectionProfiles property from the Connectivity class allows us to understand the type of connection in more detail, as demonstrated in the following example:
var profiles = Connectivity.ConnectionProfiles;
if(profiles.Contains(ConnectionProfile.WiFi))
{
// WiFi connection
}
This is very useful because it allows us to understand the following type of connections: WiFi, Ethernet, Cellular, Bluetooth, and Unknown. The Connectivity class also exposes the ConnectivityChanged event, which is raised when the status of the connection changes. You can declare a handler in the usual way:
Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged;
Then you can leverage the ConnectivityChangedEventArgs object to understand what happened and react accordingly:
private async void Connectivity_ConnectivityChanged(object sender,
ConnectivityChangedEventArgs e)
{
if(e.NetworkAccess != NetworkAccess.Internet)
{
await DisplayAlert("Warning", "Limited internet connection", "OK");
// Do additional work to limit network access...
}
}
As you can see, this feature simplifies one of the most important tasks an app must perform, and that would otherwise require writing specific code for each platform.
It is common to include hyperlinks within the user interface of an app. Xamarin.Essentials provides the Browser class, which makes it simple to open URIs inside the system browser. You use it as follows:
await Browser.OpenAsync("https://www.microsoft.com");
Xamarin.Essentials also offers the Launcher class, which allows for opening any kind of URIs with the system default app. For example, the following code opens the default email client:
string uri = "mailto://[email protected]";
var canOpen = await Launcher.CanOpenAsync(uri);
if(canOpen)
await Launcher.OpenAsync("mailto://[email protected]");
It is good practice to check if the system supports opening the specified URI via the CanOpenAsync method; if this returns true, you can then invoke OpenAsync to open the specified URI inside the default system app.
Sending SMS messages is very straightforward with Xamarin.Essentials. Look at the following code:
public async Task SendSms(string messageText, string[] recipients)
{
var message = new SmsMessage(messageText, recipients);
await Sms.ComposeAsync(message);
}
The SmsMessage class needs the message text and a list of recipients in the form of an array of string objects. Then the ComposeAsync from the Sms class will open the system UI for sending messages. Without Xamarin.Essentials, this would require working with the dependency service, and with three different platform-specific implementations. With Xamarin.Essentials, you accomplish the same result with two lines of code.
If a feature is not available on the current device and system, Xamarin.Essentials’s types raise a FeatureNotSupportedException. It is recommended to always handle this exception in a try..catch block to avoid unexpected app crashes.
As you can imagine, it is not possible to provide examples for all the features wrapped by the Xamarin.Essentials library inside a book of the Succinctly series. The complete list of features with examples is available in the official documentation page, which is regularly updated when new releases are out.
In previous sections, you looked at how to interact with native Android, iOS, and Windows features by accessing their APIs directly in C# code or through plugins. In this section, you will 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.
Xamarin.Forms allows you to add native views directly into the XAML markup. This feature is a recent addition, and it makes it really easy to use native visual elements. To understand how native views in XAML work, consider Code Listing 36.
Code Listing 36
<?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" xmlns:win="clr-namespace:Windows.UI.Xaml.Controls; assembly=Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows" 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}" /> <win:TextBlock Text="Native Text"/> </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 36, the XAML markup includes a UILabel native label on iOS, a TextView native label on Android, and a TextBlock native view on Windows. 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 good news is that you can also assign native properties to each view directly in your XAML.
Renderers are classes that Xamarin.Forms uses to access and render native views, and that bind the Xamarin.Forms 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, TextView, and TextBlock views on iOS, Android, and Windows, respectively. Xamarin.Forms views completely depend on renderers to expose their look and behavior. The good news is that you can override the default renderers with the custom renderers, which you can use to extend or override features in the Xamarin.Forms views. A custom renderer is 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 help you better understand custom renderers.
Suppose you want an Entry view to autoselect 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 .NET Standard 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, Xamarin.Android, and Universal Windows Platform documentation.
Code Listing 37 shows how to implement a custom renderer in iOS, Code Listing 38 shows the Android version, and Code Listing 39 shows the Windows version.
Code Listing 37
[assembly: ExportRenderer(typeof(AutoSelectEntry), typeof(AutoSelectEntryRenderer))] namespace App1.iOS { public class AutoSelectEntryRenderer: EntryRenderer { 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 38
using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using NativeAccess; using NativeAccess.Droid;
[assembly: ExportRenderer(typeof(AutoSelectEntry), typeof(AutoSelectEntryRenderer))] namespace App1.Droid { public class AutoSelectEntryRenderer: EntryRenderer { public AutoSelectEntryRenderer(Context context): base(context) { } protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) { base.OnElementChanged(e); if (e.OldElement == null) { var nativeEditText = (global::Android.Widget.EditText)Control; nativeEditText.SetSelectAllOnFocus(true); } } } } |
Code Listing 39
using App1; using App1.UWP; using Xamarin.Forms; using Xamarin.Forms.Platform.UWP;
[assembly: ExportRenderer(typeof(AutoSelectEntry), typeof(AutoSelectEntryRenderer))] namespace App1.UWP { public class AutoSelectEntryRenderer: EntryRenderer { protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) { base.OnElementChanged(e); if (e.OldElement == null) { var nativeEditText = Control; nativeEditText.SelectAll(); } } } } |
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. The ExportRenderer attribute at the assembly level 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 40.
Code Listing 40
<?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 defined 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.
Effects 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 .NET Standard (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 you know they exist. You can check out the official documentation, which explains how to consume built-in effects and create custom ones.
Xamarin.Forms provides the platform-specifics, which allow for consuming features that are available only on specific platforms. Platform-specifics represent a limited number of features, but they allow you to work without implementing custom renderers or effects.
Note: Platform-specifics do not represent features that are available cross-platform. They instead provide quick access to features that are available only on specific platforms. As an additional clarification, a platform-specific might be available on iOS, while the same platform-specific might not exist for Android and UWP.
For instance, suppose you are working on an iOS app and you want the separator of a ListView to be full width (which is not the default). Without platform-specifics, you would need to implement a custom renderer to accomplish this. With platform-specifics, you just need the code shown in Code Listing 41.
Code Listing 41
<?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:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" x:Class="NativeAccess.PlatformSpecificsPage"> <ContentPage.Content> <StackLayout> <ListView ios:ListView.SeparatorStyle="FullWidth" x:Name="ListView1"> <!-- Bind your data and add a data template here... --> </ListView> </StackLayout> </ContentPage.Content> </ContentPage> |
In the case of iOS, you need to import the Xamarin.Forms.PlatformConfiguration.iOSSpecific namespace. Then you can use attached properties provided by this namespace on the view of your interest. In the example shown in Code Listing 37, the attached property ListView.SeparatorStyle allows you to customize the separator width.
Platform-specifics can also be used in C# code. In this case, you need two using directives to import the Xamarin.Forms.PlatformConfiguration and Xamarin.Forms.PlatformConfiguration.iOSSpecific. Then you can invoke the On method on the view of your interest, passing the target platform, and supplying the platform-specific implementation you need. The following code provides an example that represents the same scenario seen in Code Listing 37, but in C# code:
this.ListView1.On<iOS>().SetSeparatorStyle(SeparatorStyle.FullWidth);
Platform-specifics work the same way on Android and UWP. In the case of Android, the namespace you import is Xamarin.Forms.PlatformConfiguration.AndroidSpecific (for both XAML and C#), whereas for UWP the namespace is Xamarin.Forms.PlatformConfiguration.WindowsSpecific. The list of built-in platform-specifics in Xamarin.Forms is available in the documentation.
Tip: A platform-specific for one platform will simply be ignored on other platforms.
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, Android, and Windows 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 .NET Standard library.
With Xamarin.Essentials, you have ready-to-use cross-platform abstractions for the most common scenarios, such as (but not limited to) accessing sensors, the network information, settings, or battery status. In terms of native visual elements, you can embed iOS, Android, and Windows native views directly in your XAML. You can also write custom renderers or effects to change the look and feel of your views, and you can use platform-specifics to quickly implement a few features that are available only on specific platforms. 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.