nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Prism for Xamarin.Forms入門 よりみち:DependencyService

今回はPrism.Formsから少し離れて、Xamarin.Formsの提供するDependencyServiceについて解説します。
と言うのも
「Prism.Forms上でDependencyServiceとどう付き合うか?」
と言った内容のエントリーを書こうと考えていたのですが、当然ですがDependencyServiceの知識無しに説明するのは困難です。
また、PrismからDependencyServiceを利用する方法を説明するにしても、具体的なコードがないと説明が難しいです。

そこで、「DependencyService with Prism」を投稿する前に、Xamarin.FormsのDependencyServiceについて簡単にまとめることにしました。
まずは通常のDependencyServiceの利用方法を説明した後、次のエントリーにて課題と、その課題に対するPrismのアプローチについて説明したいと思います。

なお、サンプル実装の中身自体は、公式やPrismのDependencyServiceのドキュメントとほぼ変わりありません。
そのため、DependencyServiceについて十分に理解されている方は、本エントリーはスキップして次のエントリーをお読みください。

www.nuits.jp

なお、Prism for Xamarin.Forms入門、以下に目次がありますので、他のエントリーもご覧いただけると嬉しいです。
【Xamarin】Prism.Forms入門 目次 - nuits.jp blog

それでは始めましょう。

Overview

さて、DependencyServiceと言うのは、Xamarin.Formsでプラットフォーム個別の実装を利用する為の仕組みです。
Xamarin.FormsのUIは、基本的にPortable Class Library(PCL)もしくはShared Class Libraryとして作成されます。
DependencyServiceとは
「プラットフォーム毎に異なる実装を、共通コードから呼び出す仕組み」
と言うと分かりやすいかもしれません。

これが実は少し曲者です。
以下の図は今回作成するプロジェクトの依存関係を図示したものです。
それぞれのプラットフォーム用のプロジェクトが、PCLプロジェクトに対して依存しているのが見て取れます。

f:id:nuitsjp:20160911183029p:plain

この為、直接PCLプロジェクトから、固有の機能を利用する為に、直接それぞれのプラットフォーム用プロジェクトを参照するわけにはいきません。
循環参照になってしまいますからね。

本エントリーでは、まずは簡単なDependencyServiceの実装方法を紹介します。
作るものは、入力された文字列を喋らせる、「Text to Speech」を題材に説明したいと思います。
その上で、DependencyServiceがどういった構造になっているのか、概略を理解していただこうと思います。

Quickstart

それでは実際に作成していきましょう。
まずは、Prism Template Packから新しいプロジェクトを作成します。
今回のプロジェクトは、TextSpeakerという名称で作ります。
プロジェクトの作成の仕方は、良かったら以下の記事も参考にしてください。

www.nuits.jp

なお今回は、iOS・Android・UWPを対象とします。
Githubにサンプル実装を公開していますので、もし良かったらそちらを見ていただいても良いかもしれません。

github.com

こちらのTextSpeaker.slnを参照してください。

共通インターフェースの定義

さて、プロジェクトを作成したら、まずは以下の手順で文字列から音声に変換するためのインターフェースを作成します。

  1. PCLプロジェクト「TextSpeaker」直下に「Model」フォルダを作成する
  2. 「Model」フォルダの下に、「ITextToSpeech」というインターフェースを作成する

作成したITextToSpeechファイルには以下のようにstringを引数に取るSpeechメソッドを作成します。

namespace TextSpeaker.Model
{
    public interface ITextToSpeech
    {
        void Speak(string message);
    }
}

続いて各プラットフォーム用の実装クラスを以下の手順で作成していきます。

  1. 各プラットフォーム(例えばTextSpeaker.iOS)の下に「Model」フォルダを作成する
  2. 作成した「Model」フォルダの下にそれぞれTextToSpeechクラスを作成する

クラスを作成したら、それぞれのクラスでITextToSpeechインターフェースを実装していきます。

TextToSpeech.cs:iOS

以下がiOS版のコードとなります。

[assembly: Xamarin.Forms.Dependency(typeof(TextToSpeech))]
namespace TextSpeaker.iOS.Model
{
    public class TextToSpeech : ITextToSpeech
    {
        public void Speak(string message)
        {
            var speechSynthesizer = new AVSpeechSynthesizer();

            var speechUtterance = new AVSpeechUtterance(message)
            {
                Rate = AVSpeechUtterance.MaximumSpeechRate / 4,
                Voice = AVSpeechSynthesisVoice.FromLanguage("en-US"),
                Volume = 0.5f,
                PitchMultiplier = 1.0f
            };

            speechSynthesizer.SpeakUtterance(speechUtterance);
        }
    }
}

Speakメソッドの中身についての説明は今回は割愛しますが、重要なポイントが2つあります。

  1. [assembly: Xamarin.Forms.Dependency(typeof(TextToSpeech))] 宣言
  2. ITextToSpeechインターフェースの実装

この二つの情報から、Xamarin.Formsが 「あ、これがiOS用のITextToSpeechの実装クラスなんだな」
と判断します。
特に1番目の宣言を(私が)忘れがちなので、気をつけて下さい。
また、Dependencyクラスは同名のクラスがUnityの中にも存在するため気をつけてください。
ここで利用するのは、Xamarin.Forms.Dependencyクラスです。

TextToSpeech.cs:Android

続いては、Android番の実装です。

[assembly: Xamarin.Forms.Dependency(typeof(TextSpeaker.Droid.Model.TextToSpeech))]
namespace TextSpeaker.Droid.Model
{
    public class TextToSpeech : Java.Lang.Object, ITextToSpeech, Android.Speech.Tts.TextToSpeech.IOnInitListener
    {
        private Android.Speech.Tts.TextToSpeech _speaker;
        private string _message;

        public void Speak(string message)
        {
            var c = Forms.Context;
            _message = message;
            if (_speaker == null)
            {
                _speaker = new Android.Speech.Tts.TextToSpeech(c, this);
            }
            else
            {
                var p = new Dictionary<string, string>();
                _speaker.Speak(_message, QueueMode.Flush, p);
            }
        }

        public void OnInit(OperationResult status)
        {
            if (status == OperationResult.Success)
            {
                var p = new Dictionary<string, string>();
                _speaker.Speak(_message, QueueMode.Flush, p);
            }
        }
    }
}

最初、Java.Lang.Objectを継承していなかったため、OnInitがコールバックされず、1回目の実行時に_speaker.Speakが実行できず少し悩みました。
悩んでいたら、「Xamarinの中の人」がサクっと指摘してくれて直りました。さすがや。

TextToSpeech.cs:UWP

つづいてUWP板です。

[assembly: Xamarin.Forms.Dependency(typeof(TextToSpeech))]
namespace TextSpeaker.UWP.Model
{
    public class TextToSpeech : ITextToSpeech
    {
        public async void Speak(string message)
        {
            using (var synth = new SpeechSynthesizer())
            using (var stream = await synth.SynthesizeTextToStreamAsync(message))
            {
                var mediaElement = new MediaElement();
                mediaElement.SetSource(stream, stream.ContentType);
                mediaElement.Play();
            }
        }
    }
}

MainPageViewModel.cs

共通のインターフェースと、プラットフォーム個別の実装が完了したところで、次はViewModelから作成したITextToSpeechを呼び出すICommandを実装します。
MainPageViewModel.csを開き、以下のようにコマンドプロパティを追加して、コンストラクタに対象コマンドの初期化コードを追加してください。

public ICommand SpeachCommand { get; }
public MainPageViewModel()
{
    SpeachCommand = new DelegateCommand(() =>
    {
        DependencyService.Get<ITextToSpeech>().Speak(Title);
    });
}

大切な点は以下のDependencyServiceのGetメソッドの呼び出しです。

DependencyService.Get<ITextToSpeech>().Speak(Title);

Xamarin.Forms.DependencyServiceクラスのGetメソッドに取得したいインターフェースを指定することで、実行時のプラットフォーム用のインスタンスを取得することができます。
この際、実装クラスの特定は先ほど説明したDependencyAttributeによって行なわれます。
このため、インターフェースを実装していても、DependencyAttributeが指定されていないと正しく取得されないため気をつけてください。

MainPage.xaml

最後です。
MainPageにボタンを追加して、ViewModelのコマンドにバインドします。
ついでなので、LabelをEntryに変更して、喋らせる内容を変更できるようにしましょう。

<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="TextSpeaker.Views.MainPage"
             Title="MainPage">
  <StackLayout HorizontalOptions="Fill" VerticalOptions="Center" Margin="10">
    <Entry Text="{Binding Title}" HorizontalOptions="Fill"/>
    <Button Text="Speach" Command="{Binding SpeachCommand}"/>
  </StackLayout>
</ContentPage>

実行

というわけで、実装は以上です。
うまく実装できていれば、下のような画面が表示されて、ボタンを押下するとテキストを読み上げてくれるはずです。

f:id:nuitsjp:20160911183855p:plain

また今回実装した内容をクラス図に落としたものが以下になります。

f:id:nuitsjp:20160905005523p:plain

どこに課題があるか、鋭い人は気が付いたでしょうか?

と言う分けで、今回はここまでです。
次回はこれをベースに、課題の解説と、その課題に対するPrismのアプローチを説明したいと思います。
それではまた!