Visual Studio Community 2015 でAndroidアプリを作成する(Xamarin)

普段はモバイル開発とは全く無縁だが、Xamarinが無償提供になったようなのでとりあえずどんなものか試してみる。
ドキュメントは、Creating Mobile Apps with Xamarin.Forms Book First Edition - Xamarin からPDFがダウンロードできる。これがあれば大体のことはできそうだが、1000ページ超でサクッと試すには不向き。

とっかかりやすいチュートリアルとして、Getting Started with Xamarin.Forms - Xamarinを試してみる。
「Hello, Xamarin.Forms」ー「Quickstart」から始める。


  1. 新規プロジェクトの作成
    VisualStudioを起動して、新規プロジェクトを作成する。
    f:id:foohogehoge:20160427214454p:plain
  2. テンプレートの選択
    新しいプロジェクト ダイアログで、「Blank App (Xamarin.Forms Portable)」を選択する。
    プロジェクト名は、チュートリアルに従って「Phoneword」とした。
    f:id:foohogehoge:20160427214715p:plain
  3. Windowsのプラットフォーム
    f:id:foohogehoge:20160427215004p:plain
    チュートリアルには記載されていないが、Windowsのプラットフォームを選択するダイアログがある。2つしかバージョンが選択できなかったので、Target Versionを新しいほう/Minimum Version を古いほうにした。
  4. 空のプロジェクトが作成された
    f:id:foohogehoge:20160427215641p:plain
    なぜか無数のエラーが……ほとんどUWPから発生しているようだ。。。
  5. エラーを解消していく
    以前にニュースリーダーか何かで「テンプレートから作成した直後はエラーの嵐」だとチラ見した記憶があったので適当にググる。下記サイトがヒットしたので参考にさせていただく。
    ytabuchi.hatenablog.com

  6. ソリューションを右クリックして「NuGetパッケージの復元」
    f:id:foohogehoge:20160427221514p:plain

  7. PCLプロジェクトのリビルド

    PCL プロジェクトを[リビルド]

    だそうだが、PCL???と思ったので調べてみると下記が見つかった。
    PCL (Portable Class Library) - Xamarin 3 の新しいコード共有テクニック : XLsoft エクセルソフト
    PCLとは全プラットフォームで共通して使えるライブラリのことで、”移植可能”と書いてあるプロジェクトのようだ。PCL+固有プラットフォームのプロジェクト の組み合わせでアプリケーションを構築する……のかな。。。
    リビルドすると問題なく成功する。


  8. UWPをリビルドする。これも問題なく成功した。
    警告が10個残っているが、とりあえず無視して先へ進む。

  9. PCLプロジェクトを右クリックして新規項目を追加を選択

    f:id:foohogehoge:20160427223500p:plain


  10. Visual C# > Cross-Platform から Forms XML Page を選択。 名前はチュートリアルに従って ”MainPage” とする。

    f:id:foohogehoge:20160427224034p:plain


  11. 追加した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"
                 x:Class="Phoneword.MainPage">
    	<ContentPage.Padding>
    		<OnPlatform x:TypeArguments="Thickness"
                        iOS="20, 40, 20, 20"
                        Android="20, 20, 20, 20"
                        WinPhone="20, 20, 20, 20" />
    	</ContentPage.Padding>
    	<ContentPage.Content>
    		<StackLayout VerticalOptions="FillAndExpand"
                         HorizontalOptions="FillAndExpand"
                         Orientation="Vertical"
                         Spacing="15">
    			<Label Text="Enter a Phoneword:" />
    			<Entry x:Name="phoneNumberText" Text="1-855-XAMARIN" />
    			<Button x:Name="translateButon" Text="Translate" Clicked="OnTranslate" />
    			<Button x:Name="callButton" Text="Call" IsEnabled="false" Clicked="OnCall" />
    		</StackLayout>
    	</ContentPage.Content>
    </ContentPage>
    


  12. ソリューションエクスプローラーでMainPage.xamlを展開して、MainPage.xaml.csを開く

    f:id:foohogehoge:20160427230019p:plain


  13. MainPage.xaml.csの内容を下記で置き換える

    using System;
    using System.Threading.Tasks;
    using Xamarin.Forms;
    
    namespace Phoneword {
        public partial class MainPage : ContentPage {
            string translatedNumber;
    
            public MainPage() {
                InitializeComponent();
            }
    
            void OnTranslate(object sender, EventArgs e) {
                translatedNumber = Core.PhonewordTranslator.ToNumber(phoneNumberText.Text);
                if (!string.IsNullOrWhiteSpace(translatedNumber)) {
                    callButton.IsEnabled = true;
                    callButton.Text = "Call " + translatedNumber;
                }
                else {
                    callButton.IsEnabled = false;
                    callButton.Text = "Call";
                }
            }
    
            async void OnCall(object sender, EventArgs e) {
                if (await this.DisplayAlert(
                        "Dial a Number",
                        "Would you like to call " + translatedNumber + "?",
                        "Yes",
                        "No")) {
                    var dialer = DependencyService.Get<IDialer>();
                    if (dialer != null)
                        dialer.Dial(translatedNumber);
                }
            }
        }
    }
    

    エラーが出るが、この後のステップで解消されそうなので無視して次へ進む。


  14. ソリューションエクスプローラーからApp.csを開いて下記の内容で置き換える。

    using System;
    using Xamarin.Forms;
    
    namespace Phoneword {
        public class App : Application {
            public App() {
                MainPage = new Phoneword.MainPage();
            }
    
            protected override void OnStart() {
                // Handle when your app starts
            }
    
            protected override void OnSleep() {
                // Handle when your app sleeps
            }
    
            protected override void OnResume() {
                // Handle when your app resumes
            }
        }
    }
    


  15. PCLプロジェクトにC#のクラスを新規追加する。名前は”PhoneTranslator”として内容を下記で置き換える。

    using System.Text;
    
    namespace Core {
        public static class PhonewordTranslator {
            public static string ToNumber(string raw) {
                if (string.IsNullOrWhiteSpace(raw))
                    return null;
    
                raw = raw.ToUpperInvariant();
    
                var newNumber = new StringBuilder();
                foreach (var c in raw) {
                    if (" -0123456789".Contains(c))
                        newNumber.Append(c);
                    else {
                        var result = TranslateToNumber(c);
                        if (result != null)
                            newNumber.Append(result);
                        // Bad character?
                        else
                            return null;
                    }
                }
                return newNumber.ToString();
            }
    
            static bool Contains(this string keyString, char c) {
                return keyString.IndexOf(c) >= 0;
            }
    
            static readonly string[] digits = {
                "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ"
            };
    
            static int? TranslateToNumber(char c) {
                for (int i = 0; i < digits.Length; i++) {
                    if (digits[i].Contains(c))
                        return 2 + i;
                }
                return null;
            }
        }
    }
    


  16. PCLプロジェクトに IDialer インターフェイスを追加して下記の内容とする。

    using System;
    
    namespace Phoneword
    {
        public interface IDialer
        {
            bool Dial(string number);
        }
    }
    


  17. 次のステップはiPhon用のコーディングだが、実機がないのでパスしてAndroidの項へ飛ぶ。と、その前にMainPage.xaml.csでコンパイルエラーが出ていて気になるので解決しておく。
    チュートリアルはここから各プラットフォームのコーディングを行って終了のようだ。
    つまり、現時点で共通部分であるPCLプロジェクトにコンパイルエラーが出ているのはマズいはずである。
    「InitializeComponentメソッドがない」と怒られている。WindowsForm開発でもなじみのあるメソッドだが、どこに実装されているのか?おそらくMainPage.xamlあたりにあるはずだが……探してもない。コピペミスを疑ってチュートリアルからMainPage.xamlを再度貼り付けなおしたらエラーが消えた。
    InitializeComponentの実体を探すとobj\Debugの下に「Phoneword.MainPage.xaml.g.cs」が作成されておりそこに定義してあった。
    ……このエラーはこの後のステップでも度々発生するが、xamlを編集して保存するとOKな様子。

  18. Androidプロジェクトに PhoneDialer クラスを追加して以下の内容で置き換える。

    using Android.Content;
    using Android.Telephony;
    using Phoneword.Droid;
    using System.Linq;
    using Xamarin.Forms;
    
    using Uri = Android.Net.Uri;
    
    [assembly: Dependency(typeof(PhoneDialer))]
    
    namespace Phoneword.Droid {
        public class PhoneDialer : IDialer {
            public bool Dial(string number) {
                var context = Forms.Context;
                if (context == null)
                    return false;
    
                var intent = new Intent(Intent.ActionCall);
                intent.SetData(Uri.Parse("tel:" + number));
    
                if (IsIntentAvailable(context, intent)) {
                    context.StartActivity(intent);
                    return true;
                }
    
                return false;
            }
    
            public static bool IsIntentAvailable(Context context, Intent intent) {
                var packageManager = context.PackageManager;
    
                var list = packageManager.QueryIntentServices(intent, 0)
                    .Union(packageManager.QueryIntentActivities(intent, 0));
    
                if (list.Any())
                    return true;
    
                var manager = TelephonyManager.FromContext(context);
                return manager.PhoneType != PhoneType.None;
            }
        }
    }
    


  19. ソリューションエクスプローラーでAndroidプロジェクトのプロパティをダブルクリック
    f:id:foohogehoge:20160428004216p:plain

  20. Android Manifest を選択し、Required permissionsから「CALL_PHONE」にチェックする。
    f:id:foohogehoge:20160428004622p:plain
    また、Package name が空だとエラーダイアログが表示されるので、Package name に "Phoneword" と入力する。

  21. 他のプラットフォームは追々試すとして、とりあえずAndroidだけビルドしてみる。
    「"ConvertResourcesCases"タスクが予期せずに失敗しました」

  22. ググったところ、ソリューションのパスに日本語が含まれているとNGらしいのでソリューションを移動してリトライしたところうまくビルドできた。

  23. 普段使用しているNexsus5を開発者モードにし、USBデバッグ可にしてPCと接続する。
    開発者モードは、Nexsus5の設定>端末情報 で表示されるビルド番号を連打すると使用できるようになる。
    設定>開発者向けオプションからUSBデバッグをONにする。

  24. VisualStudioでAndroidプロジェクトをスタートアッププロジェクトに設定し、デバッグ実行する。
    しばらくすると、Nexsus側に画面が表示された。

    f:id:foohogehoge:20160428013902j:plain