ぴーさんログ

だいたいXamarin.Formsのブログ

Plugins for Xamarinを作ろう!

前置き

Build 2016での発表から全ての開発者がVisual StudioでXamarinを利用できるようになり、アプリのみならずクロスプラットフォームなライブラリ開発も行いやすくなりました。

という訳で今回はPlugins for XamarinのテンプレートからNuGetパッケージを作るまでの流れを解説します。

Plugins for Xamarin って何?

Xamarin.iOS, Xamarin.Android, Windows Phone などの固有機能を共通のインターフェースで利用できるようにデザインされたライブラリ群です。 代表的なものにバッテリー、ストレージ、位置情報などを利用するためのプラグインがあります。

Plugins for Xamarinを 使いこなす 方法はBuild Insiderで田淵さん(@ytabuchi)連載している記事を参照してください。

Plugins for Xamarinを使いこなすには?(ファイルシステム編) - Build Insider 他

環境準備

  • Visual Studio 2015 Update 2
  • Xamarin SDK
  • Plugin For Xamarin用のプロジェクトテンプレート
  • Windows 10推奨(UWPプロジェクトを含むので)

(筆者の環境はWindows 10 & Visual Studio Enterprise 2015 with Update 2です)

まずはVisual StudioとXamarinをインストールしてiOSAndroidアプリがビルドできる環境を整えてください。 (先駆者達の環境構築記事を見ながら頑張りましょう)

次にGitHubにあるPlugin For Xamarinのリポジトリを開きます。

xamarin/plugins: Plugins for Xamarin

「Tools to get Started」以下のリンクからプロジェクトテンプレートを入手してください。 (一応こちらにもリンクを張っておきます。)

プロジェクトを作ろう!

準備が整ったら新規プロジェクトを作成しましょう。 テンプレート名は「Plugin for Xamarin」です。("plugin"で絞り込みをかけると楽)

f:id:ticktack623:20160407233019j:plain

ソリューション名は"Battery"や"Geolocator"のような 機能名 にするのがお約束です。

続いて生成されたプロジェクトを眺めてみましょう。

f:id:ticktack623:20160407233839j:plain

プロジェクトが沢山あってびっくりしますが、以下の3種類があると押さえておけば問題ありません。

  • Plugin.機能名 (PCL)
  • Plugin.機能名.Abstractions (PCL)
  • Plugin.機能名.プラットフォーム名 * n個

Plugin.機能名.Abstractions (PCL)

インターフェースやenumを定義します。 出力は Plugin.機能名.Abstractions.dll となります。

Plugin.機能名 (PCL)

PCLプロジェクト用のダミー実装を定義します。「なんのこっちゃ?」と思うかもしれませんが、実はこれが無いとPCLプロジェクトでPluginを参照するときに困ります。 出力は Plugin.機能名.dll となります。

Plugin.機能名.プラットフォーム名

各プラットフォーム用の実装を定義します。 出力は Plugin.機能名.dll となります。

プラグインを実装しよう!

この記事ではサンプルとして、アプリケーションのバージョンを取得するプラグインを実装します。 (not assembly version)

Plugin.機能名.Abstractions (PCL) プロジェクト

Plugin.機能名.AbstractionsプロジェクトにはInterfaceやenum、Event等を定義します。

テンプレートから生成された IAwesomeFeature.cs にコードを追加します。

using System;

namespace Plugin.AwesomeFeature.Abstractions
{
  /// <summary>
  /// Interface for AwesomeFeature
  /// </summary>
  public interface IAwesomeFeature
  {
        // 追加したコード
        string GetAppVersion();
  }
}

Plugin.機能名 (PCL) プロジェクト

Plugin.機能名プロジェクトにはPCLプロジェクトから参照するためのダミー実装となります。 Cross機能名.cs だけが生成されており、プラグインをsingletonなAPIラッパーとする限りコードを変更する必要はありません。

using Plugin.AwesomeFeature.Abstractions;
using System;

namespace Plugin.AwesomeFeature
{
  /// <summary>
  /// Cross platform AwesomeFeature implemenations
  /// </summary>
  public class CrossAwesomeFeature
  {
    static Lazy<IAwesomeFeature> Implementation = new Lazy<IAwesomeFeature>(() => CreateAwesomeFeature(), System.Threading.LazyThreadSafetyMode.PublicationOnly);

    /// <summary>
    /// Current settings to use
    /// </summary>
    public static IAwesomeFeature Current
    {
      get
      {
        var ret = Implementation.Value;
        if (ret == null)
        {
          throw NotImplementedInReferenceAssembly();
        }
        return ret;
      }
    }

    static IAwesomeFeature CreateAwesomeFeature()
    {
#if PORTABLE
        return null;
#else
        return new AwesomeFeatureImplementation();
#endif
    }

    internal static Exception NotImplementedInReferenceAssembly()
    {
      return new NotImplementedException("This functionality is not implemented in the portable version of this assembly.  You should reference the NuGet package from your main application project in order to reference the platform-specific implementation.");
    }
  }
}

Plugins for Xamarinは Plugin.機能名.Cross機能名.Current でアクセスするようデザインされており、 Currentプロパティにアクセスした際にプラットフォーム実装が無ければ例外となります。 (具体的には、対象プラットフォームにプラグイン実装が提供されていない場合、PCLプロジェクトにプラグインのNuGetパッケージを追加したのにプラットフォーム側のプロジェクトでパッケージ追加を忘れた場合)

ちなみに、各プラットフォームプロジェクトはこの Cross機能名.cs をリンク参照しています。

Plugin.機能名.プラットフォーム名 プロジェクト

今回はAndroid版にだけ機能を実装することにします。

各プラットフォームのプロジェクトには 機能名Implementation.cs ファイルが生成されているので、ここにコードを追加します。

using Plugin.AwesomeFeature.Abstractions;
using System;

namespace Plugin.AwesomeFeature
{
  /// <summary>
  /// Implementation for Feature
  /// </summary>
  public class AwesomeFeatureImplementation : IAwesomeFeature
  {
        // インターフェースの実装を追加
        public string GetAppVersion()
        {
            var context = Android.App.Application.Context;
            return context.PackageManager.GetPackageInfo(context.PackageName, 0).VersionName;
        }
    }
}

NuGetパッケージを作ろう!

ソリューションを右クリックから新規項目を追加。

「Plugins for Xamarin NuSpec」を追加、名前はソリューションと同じく機能名にします。 ("nuspec"でフィルタリングすると楽)

f:id:ticktack623:20160408141828j:plain

f:id:ticktack623:20160408142526j:plain

今回は動作確認のため、NuGet.orgに公開しないローカルなNuGetパッケージとして作成します。nuspecやNuGetパッケージの作り方に関しては、動作確認に必要な最低限の解説に留めます。(私にとってnugetは複雑で難しいので)

nuspecファイルを編集します。

  • <licenseUrl /><projectUrl/>
    • 必須項目なので架空のUrlで埋めておきます。
  • <files></files>

準備が整ったら、各プロジェクトをビルドしてパッケージマネージャコンソールで次のコマンドを実行します。

nuget pack

成功するとソリューションフォルダの直下にNuGetパッケージファイル(nupkg)が出力されます。

f:id:ticktack623:20160408145421j:plain

f:id:ticktack623:20160408145727j:plain

NuGetパッケージの作成はMacでも可能ですが、Windows版でしか使えない機能($id$等のマクロ、MS Buildに依存してる)があったりするのでWindowsで行った方が何かと便利でしょう。

NuGetパッケージを使おう!

それでは出来上がったNuGetパッケージをローカルとして動作確認してみましょう。

まず、任意のフォルダにnupkgファイルを配置します。(例では C:\PackageSource )

続いて動作確認用のプロジェクト(「Blank App(Xamarin.Forms Portable)」)を新規作成します。

ツール > NuGetパッケージ マネージャー > パッケージ マネージャー設定 を開きます。

f:id:ticktack623:20160408152047j:plain

「パッケージ ソース」に新しいパッケージソースを追加修正します。

  • 名前: 任意
  • ソース: nupkgファイルを配置したフォルダ

PCLプロジェクトとAndroidプロジェクトにNuGetパッケージを追加します。この際、パッケージソースを先ほど作ったローカルフォルダに変更してください。

f:id:ticktack623:20160408152759j:plain

PCLプロジェクトのAppクラスを動作確認用に修正します。

public App()
{
    var button = new Button
    {
        Text = "Show dialog"
    };
    var page = new ContentPage
    {
        Content = new StackLayout
        {
            VerticalOptions = LayoutOptions.Center,
            Children = {
                button,
            }
        }
    };

    button.Clicked += (s, e) =>
    {
        page.DisplayAlert("App version" ,
            Plugin.AwesomeFeature.CrossAwesomeFeature.Current.GetAppVersion(),
            "OK");
    };

    MainPage = page;
}

早速Androidを実行してみます。

f:id:ticktack623:20160408161146j:plain:w220 f:id:ticktack623:20160408161204j:plain:w220

ばっちりAndroid固有の実装が動いています!

それでは固有実装を用意していないUWP版を実行するとどうなるでしょう?

f:id:ticktack623:20160408154647j:plain:w400 f:id:ticktack623:20160408154751j:plain

エラー無くビルドできましたが、ボタンをクリックするとNotImplementedExceptionが発生しました。 これはPCLプロジェクトからダミー実装のdllを引き継いだためですね。

Bait and Switch

さて、PCLプロジェクトの出力であるdll(ここではApp3.dll)はAndroid、UWPの両方に全く同じものが同梱されています。 そして、PluginのdllはAndroidとUWPでそれぞれ違うものが同梱されています。(Android:固有実装、UWP:ダミー実装)

これの何が凄いかというと、PCLプロジェクトが「ビルド時に参照していたdll」と「実行時に参照するdll」をすり替えたにもかかわらず、メソッド呼び出しができている点です。 これは「Bait and Switch」と呼ばれるトリックです。(assembly name、version、class構造が一致していればいいらしい?)

「Bait and Switch」実現のテンプレート化こそがPlugins for Xamarinの真骨頂であると言えます。

Androidアプリに固有実装dllが同梱されている点も重要だと思うんですが、これはリンカーの仕様なんでしょうか? (同名dllが自プロジェクトと外部プロジェクト(PCL)で参照されていたら、自プロジェクトの方を優先?)

最後に

NuGetパッケージの仕様など、説明しきれない部分もありましたがPlugins for Xamarinがどんなものか何となく理解していただければ幸いです。

ぜひともクールなプラグインを作ってNuGet公開にチャレンジしてください。

↓の要求事項に応えればPlugins for Xamarinに仲間入りできるみたいですよ?

Requirements of a Plugin

  • Open source on GitHub
  • Documentation on GitHub's README file
  • Name: "FEATURE_NAME Plugin for Xamarin and Windows"
  • Namespace: Plugin.FEATURE_NAME
  • App-store friendly OSS license (we like MIT)
  • No dependency on Xamarin.Forms

リンク

xamarin/plugins: Plugins for Xamarin (GitHub)

The Bait and Switch PCL Trick