はじめに
本記事は Xamarin Advent Calendar 2018 の14日目の記事です。
今回はAndroid、iOS、UWP の各プラットフォーム固有のAPIへのアクセスを共有コードから実行するためのライブラリである「Xamarin.Essentials」を紹介させていただきます。
「Xamarin.Essentials」は、長らくプレリリースとして公開がされていましたが、今月初めに正式版1.0がリリースされました
日本語ドキュメントの1.0対応版の翻訳も、この記事を書いている間にどんどん行われていて、ほぼ完了されているようです!
Xamarin.Essentials の簡単な説明
さて、Xamarin.Essentials とは何か、簡単に説明します。
Xamarinを使うと、Android、iOS、UWP の各プラットフォーム向けのアプリ開発をC#(またはF#)のみで行うことができ、さらに共通のビジネスロジックや、Xamarin.Forms を使った際にはUIまでを共通のコードで実装することができます。
しかし、バッテリーや位置情報など、プラットフォームに依存するようなAPIを使う場合、Xamarinの標準のままではコード共有ができません。
Xamarin.Essentials は、そういったプラットフォーム依存のAPIを呼び出すためのコードを共通化するためのライブラリです!
各OS/プラットフォームのサポートバージョンは以下のとおりです。
プラットフォーム | バージョン |
---|---|
Android | 4.4 (API 19) 以上 |
iOS | 10.0 以上 |
UWP | 10.0.16299.0 以上 |
Xamarin.Essentials の機能
Xamarin.Essentials で共通のコードで実現できるプラットフォーム依存の機能は以下の29機能です(1.0時点)。
なお、1.0ではプレリリース版と比べいくつかの機能についての統廃合が行われているので、その点は注意が必要です。
機能名 | 説明 |
---|---|
加速度計 (Accelerometer) | 3次元空間内のデバイスの加速度データを取得します。 |
アプリ情報 (App Information) | アプリケーションに関する情報を見つけます。 |
バロメーター (Barometer) | 負荷の変化のバロメーターを監視します。 |
バッテリ (Battery) | バッテリ レベル、ソース、および状態を簡単に検出します。 ※1.0よりプレリリース版の「電源 (Power)」APIがここに含まれました |
クリップボード (Clipboard) | クリップボード上のテキストをすばやく簡単に設定したり読み取ったりします。 |
コンパス (Compass) | 変化のコンパスを監視します。 |
接続 (Connectivity) | 接続状態を確認し、変更を検出します。 |
デバイス ディスプレイ情報 (Device Display Information) | デバイスの画面のメトリックと向きを取得します。 ※1.0よりプレリリース版の「画面のロック (Screen Lock)」APIがここに含まれました |
デバイス情報 (Device Information) | デバイスの詳細を簡単に確認します。 |
電子メール (Email) | 電子メール メッセージを簡単に送信します。 |
ファイル システム ヘルパー (File System Helpers) | アプリ データにファイルを簡単に保存します。 |
懐中電灯 (Flashlight) | 懐中電灯のオン/オフを簡単に切り替える方法です。 |
ジオコーディング (Geocoding) | ジオコードとリバース ジオコードのアドレスおよび座標。 |
位置情報 (Geolocation) | デバイスの GPS 位置情報を取得します。 |
ジャイロスコープ (Gyroscope) | デバイスの 3 つの主軸の周りの回転を追跡します。 |
ランチャー (Launcher) | アプリケーションがシステムで URI を開くことができるようにします。 |
磁力計 (Magnetometer) | 地球の磁場を基準としたデバイスの向きを検出します。 |
メイン スレッド (MainThread) | アプリケーションのメイン スレッドでコードを実行します。 |
マップ (Maps) | 特定の場所にマップ アプリケーションを開きます。 |
ブラウザーを開く (Open Browser) | ブラウザーで特定の Web サイトをすばやく簡単に開きます。 |
向きセンサー (Orientation Sensor) | 3 次元空間内のデバイスの向きを取得します。 |
ダイヤラー (Phone Dialer) | ダイヤラーを開きます。 |
ユーザー設定 (Preferences) | 永続的なユーザー設定をすばやく簡単に追加します。 |
セキュリティで保護されたストレージ (Secure Storage) | データを安全に格納します。 |
共有 (Share) | 他のアプリにテキストや Web サイトの URI を送信します。 ※1.0よりプレリリース版の「データ転送 (Data Transfer)」APIの名称が変更になりました |
SMS | 送信用の SMS メッセージを作成します。 |
音声合成 (Text-to-Speech) | デバイス上のテキストを音声化します。 |
バージョンの追跡 (Version Tracking) | アプリケーションのバージョンとビルド番号を追跡します。 |
バイブレーション (Vibrate) | デバイスをバイブレーションさせます。 |
Xamarin.Essentials の実装
このようにさまざまな機能のコード共有化を実現する Xamarin.Essentials ですが、実装はどうなっているのでしょうか。ソースコードを見てみましょう。
https://github.com/xamarin/Essentials
中身を確認すると、Xamarin.Essentials ディレクトリの下に、各機能ごとの実装が入っています。
ソースコードはプラットフォームごとに xxxx.ios.cs
や xxxx.android.cs
などのファイルでpartialクラスに分けられていて、各プラットフォームごとの実装が書かれています。
なるほど、こういう仕組みなんですね。
例えば、「Clipboard」の実装を見てみましょう。
まずAndroid。
using System.Threading.Tasks;
using Android.Content;
namespace Xamarin.Essentials
{
public static partial class Clipboard
{
static Task PlatformSetTextAsync(string text)
{
Platform.ClipboardManager.PrimaryClip = ClipData.NewPlainText("Text", text);
return Task.CompletedTask;
}
static bool PlatformHasText
=> Platform.ClipboardManager.HasPrimaryClip;
static Task<string> PlatformGetTextAsync()
=> Task.FromResult(Platform.ClipboardManager.PrimaryClip?.GetItemAt(0)?.Text);
}
}
次にiOS。
using System.Threading.Tasks;
using UIKit;
namespace Xamarin.Essentials
{
public static partial class Clipboard
{
static Task PlatformSetTextAsync(string text)
{
UIPasteboard.General.String = text;
return Task.CompletedTask;
}
static bool PlatformHasText
=> UIPasteboard.General.HasStrings;
static Task<string> PlatformGetTextAsync()
=> Task.FromResult(UIPasteboard.General.String);
}
}
そしてUWP。
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
using WindowsClipboard = Windows.ApplicationModel.DataTransfer.Clipboard;
namespace Xamarin.Essentials
{
public static partial class Clipboard
{
static Task PlatformSetTextAsync(string text)
{
var dataPackage = new DataPackage();
dataPackage.SetText(text);
WindowsClipboard.SetContent(dataPackage);
return Task.CompletedTask;
}
static bool PlatformHasText
=> WindowsClipboard.GetContent().Contains(StandardDataFormats.Text);
static Task<string> PlatformGetTextAsync()
{
var clipboardContent = WindowsClipboard.GetContent();
return clipboardContent.Contains(StandardDataFormats.Text)
? clipboardContent.GetTextAsync().AsTask()
: Task.FromResult<string>(null);
}
}
}
各プラットフォームごとに異なる実装を、 PlatformXxxx
という名前のメソッドでラップしてくれています。
こうしてみると、Xamarin.Essentialsを直接利用せずとも、固有API呼び出しを実装する際の参考にもなりそうですね。
このそれぞれの PlatformXxxx
メソッドが、 xxxx.shared.cs
のpartialクラスに記述されたpublic な Xxxx
メソッドから呼び出されています。
using System.Threading.Tasks;
namespace Xamarin.Essentials
{
public static partial class Clipboard
{
public static Task SetTextAsync(string text)
=> PlatformSetTextAsync(text);
public static bool HasText
=> PlatformHasText;
public static Task<string> GetTextAsync()
=> PlatformGetTextAsync();
}
}
なお、.NET Standardアプリ用と思われるものは、このようになっています。現在はどのAPIでもこのように NotImplementedInReferenceAssemblyException
を投げるようになっており、中身の実装がされていません。
using System.Threading.Tasks;
namespace Xamarin.Essentials
{
public static partial class Clipboard
{
static Task PlatformSetTextAsync(string text)
=> throw new NotImplementedInReferenceAssemblyException();
static bool PlatformHasText
=> throw new NotImplementedInReferenceAssemblyException();
static Task<string> PlatformGetTextAsync()
=> throw new NotImplementedInReferenceAssemblyException();
}
}
では、これらのプラットフォームごとのpartial クラスに実装されたメソッドを、sharedのメソッドからどうやって呼び分けているのでしょうか?
それはcsprojファイルを見ればわかります。一部の抜粋ですが、
<ItemGroup>
<None Include="..\nugetreadme.txt" PackagePath="readme.txt" Pack="true" />
<PackageReference Include="mdoc" Version="5.7.4" PrivateAssets="All" />
<PackageReference Include="MSBuild.Sdk.Extras" Version="1.6.60" PrivateAssets="All" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<Compile Include="**\*.shared.cs" />
<Compile Include="**\*.shared.*.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard1.')) ">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) ">
<Compile Include="**\*.netstandard.cs" />
<Compile Include="**\*.netstandard.*.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('uap10.0')) ">
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.1.5" />
<SDKReference Include="WindowsMobile, Version=10.0.16299.0">
<Name>Windows Mobile Extensions for the UWP</Name>
</SDKReference>
<Compile Include="**\*.uwp.cs" />
<Compile Include="**\*.uwp.*.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('MonoAndroid')) ">
<Compile Include="**\*.android.cs" />
<Compile Include="**\*.android.*.cs" />
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('MonoAndroid81')) ">
<PackageReference Include="Xamarin.Android.Support.CustomTabs" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Android.Support.Core.Utils" Version="27.0.2.1" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('MonoAndroid80')) ">
<PackageReference Include="Xamarin.Android.Support.CustomTabs" Version="26.1.0.1" />
<PackageReference Include="Xamarin.Android.Support.Core.Utils" Version="26.1.0.1" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('MonoAndroid71')) ">
<PackageReference Include="Xamarin.Android.Support.CustomTabs" Version="25.4.0.2" />
<PackageReference Include="Xamarin.Android.Support.Core.Utils" Version="25.4.0.2" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('Xamarin.iOS')) ">
<Compile Include="**\*.ios.cs" />
<Compile Include="**\*.ios.*.cs" />
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
となっていて、これを見るとターゲットフレームワークに応じてビルド対象となるソースファイルを切り替えている、という仕組みだとわかります。
利用手順
では、実際に Xamarin.Essentials を使っていきたいと思います。
環境
今回は以下の環境で試しました。
- Xamarin.Forms (3.4.0)
- Xamarin.Essentials (1.0)
- Win - Visual Studio Community 2017 (15.9.3)
- Mac - Visual Studio Community 2017 for Mac (7.7.1.15)
手順
まずは準備として、
- Xamarin.Formsの空アプリを作成(XFEssentialsDemoという名前にした)
- ソリューションのNuGetパッケージの管理で、Xamarin.Forms のバージョンを最新化
したら、
- NuGet から Xamarin.Essentials をすべてのプロジェクトにインストール
します。
そしてここが重要なのですが、Android のみ、Essentials利用のために固有の設定を記述します。
ドキュメントによれば、Android プロジェクトの MainLauncher
、または起動されるすべての Activity
の OnCreate
メソッド内で Xamarin.Essentials を初期化する必要があり、また Android 上で実行時のアクセス許可を処理するために Xamarin.Essentials がすべての OnRequestPermissionsResult
を受け取る必要があり、すべての Activity
クラスへのコード追加が必要とのことです。
※iOS、UWPは設定不要です。
using System;
using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
namespace XFEssentialsDemo.Droid
{
[Activity(Label = "XFEssentialsDemo", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState); // 追加!
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
// 追加!
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
これでXamarin.Essentialsを利用する準備は完了です。
APIを利用したコードの実装
超シンプルに、呼び出しのコードを書いてみます。
試す機能は「デバイス ディスプレイ情報 (Device Display Information)」です。
まず、MainPage.xamlにボタンをつけます。
<?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:XFEssentialsDemo"
x:Class="XFEssentialsDemo.MainPage">
<StackLayout>
<Label Text="Welcome to Xamarin.Essentials!" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" />
<Button x:Name="ShowDisplayInfo" Text="Display Info" Margin="50" />
</StackLayout>
</ContentPage>
コードビハインドで、ボタンを押したらディスプレイの幅と高さをアラートする処理を書きます。
using Xamarin.Essentials;
using Xamarin.Forms;
namespace XFEssentialsDemo
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
// 以下を追記
ShowDisplayInfo.Clicked += async (sender, e) =>
{
var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
var width = mainDisplayInfo.Width;
var height = mainDisplayInfo.Height;
await DisplayAlert("Display Info", $"width: {width}px, height: {height}px", "Close");
};
}
}
}
これだけです!
動作確認
Android
iOS
UWP
3つのプラットフォーム上で、ちゃんと幅と高さが表示されました!
Xamarin.Essentials を使ったロジックのユニットテスト
ここでもうひとつライブラリをご紹介します。
Xamarin.Essentials を使ったコードをテストしやすくするための「Xamarin.Essentials.Interfaces」というライブラリです。
こちらも、NuGetからダウンロードできるようになっています。
こちらはまだちゃんと試せていないのですが、Xamarin.Essentials の各クラスをインタフェース化し、モック化可能にしてくれるライブラリで、Ryan Davisさんという方が開発しているものです。
このように Moq などと組み合わせて利用するようです。
var mockGeo = new Mock<IGeocoding>();
mockGeo.Setup(x => x.GetPlacemarksAsync(It.IsAny<double>(), It.IsAny<double>()))
.ReturnsAsync(Enumerable.Empty<Placemark>());
こちらはなんとXamarin.Essentials の最新のコードから生成されているそうで、今後も Xamarin.Essentials 本体の更新に追随して更新されていくようです。
本体のGAに合わせて、1.0がリリースされています。
こちらのサイトで、生成されたインタフェースのコードを確認できます。Azureで動いてますね。
Xamarin.Essentials 使用上の注意
ここまでに書いた話だけだと、とても素晴らしい無敵のライブラリかのように思えますが、使う際に注意が必要です。
それは、「機能によってプラットフォーム固有の設定/実装が必要になる場合がある」ことと、「各プラットフォームごとにどんな動きをするか確認しておく」ということです。
このあたりの話は、ドキュメントに詳細な記載があります。以下は「バッテリ (Battery)」のドキュメントですが、このようにプラットフォームごとのタブがドキュメントに含まれていたら要注意です。
まとめ
Xamarin.Essentials はまだまだ登場したばかりのライブラリなので、これを使った実装例などはそこまで多くありません。
使いたい機能が1つや2つだけだったり、使いたい機能でプラットフォームごとの設定をたくさんしなければならないものばかりであれば、あえて使う必要もないかもしれませんが、使い方によっては強力な武器になりそうな可能性を秘めていると思うので、機会があればこれを活かしたアプリを作ってみるのもよさそうだと思いました。