祝各位2017年事業輝煌!開年第一篇博客,繼續探索Xamarin.Forms…
為什么我做Xamarin開發的時候中意於Prism.Forms框架?本章為你揭曉。
實例代碼地址:https://github.com/NewBLife/XamarinDemo/tree/master/TextToSpeechDemo
DependencyService
1、簡介
軟件開發有一個原則叫【依賴倒置Dependence Inversion Principle 】
A.高層次的模塊不應該依賴於低層次的模塊,他們都應該依賴於抽象。
B.抽象不應該依賴於具體實現,具體實現應該依賴於抽象。
Xamarin.Forms在面對無法實現的平台特有功能時就是使用以上原則設計一個叫【DependencyService】的功能。DependencyService的目的就是讓PCL共通代碼可以調用與平台相關的功能,它使Xamarin.Forms能像原生應用一樣做任何事情!
2、工作原理
- 接口:定義功能接口在PCL類庫或者共享類庫
- 接口實現:各個平台實現接口功能
- 注冊:各個平台實現接口的類庫注冊DependencyAttribute屬性
- 調用:PCL類庫或者共享類庫調用DependencyService.Get<接口>()方法獲取平台實例對象
稍微看看原代碼了解Xamarin.Forms如何實現依賴注入
DependencyAttribute.cs文件,定義了程序集屬性標簽:
using System; namespace Xamarin.Forms { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public class DependencyAttribute : Attribute { public DependencyAttribute(Type implementorType) { Implementor = implementorType; } internal Type Implementor { get; private set; } } }
DependencyService.cs文件的Get方法(實體對象默認是單例形式存在)。
static bool s_initialized; static readonly List<Type> DependencyTypes = new List<Type>(); static readonly Dictionary<Type, DependencyData> DependencyImplementations = new Dictionary<Type, DependencyData>(); public static T Get<T>(DependencyFetchTarget fetchTarget = DependencyFetchTarget.GlobalInstance) where T : class { if (!s_initialized) Initialize(); Type targetType = typeof(T); if (!DependencyImplementations.ContainsKey(targetType)) { Type implementor = FindImplementor(targetType); DependencyImplementations[targetType] = implementor != null ? new DependencyData { ImplementorType = implementor } : null; } DependencyData dependencyImplementation = DependencyImplementations[targetType]; if (dependencyImplementation == null) return null; if (fetchTarget == DependencyFetchTarget.GlobalInstance) { if (dependencyImplementation.GlobalInstance == null) { dependencyImplementation.GlobalInstance = Activator.CreateInstance(dependencyImplementation.ImplementorType); } return (T)dependencyImplementation.GlobalInstance; } return (T)Activator.CreateInstance(dependencyImplementation.ImplementorType); }
DependencyService.cs文件的Initialize方法,遍歷所有程序集獲取標記了DependencyAttribute屬性的類型。這是不太好的地方,這樣的做法性能會大打折扣,這也是為什么不推薦使用DependencyService的一個方面。
static void Initialize() { Assembly[] assemblies = Device.GetAssemblies(); if (Registrar.ExtraAssemblies != null) { assemblies = assemblies.Union(Registrar.ExtraAssemblies).ToArray(); } Type targetAttrType = typeof(DependencyAttribute); // Don't use LINQ for performance reasons // Naive implementation can easily take over a second to run foreach (Assembly assembly in assemblies) { Attribute[] attributes = assembly.GetCustomAttributes(targetAttrType).ToArray(); if (attributes.Length == 0) continue; foreach (DependencyAttribute attribute in attributes) { if (!DependencyTypes.Contains(attribute.Implementor)) { DependencyTypes.Add(attribute.Implementor); } } } s_initialized = true; }
3,實例使用
使用TextToSpeechDemo(文本語音)實例講解如何使用DependencyService。
項目結構:
接口定義:
namespace TextToSpeechDemo { public interface ITextToSpeech { void Speak(string text); } }
Android平台實現ITextToSpeech接口:API定義
最重要的[assembly: Dependency(typeof(TextToSpeech_Android))] 這句注冊Dependency屬性。
using Android.Runtime; using Android.Speech.Tts; using System.Collections.Generic; using TextToSpeechDemo.Droid; using Xamarin.Forms; [assembly: Dependency(typeof(TextToSpeech_Android))] namespace TextToSpeechDemo.Droid { public class TextToSpeech_Android : Java.Lang.Object, ITextToSpeech, TextToSpeech.IOnInitListener { TextToSpeech speaker; string toSpeak; public TextToSpeech_Android() { } public void Speak(string text) { var ctx = Forms.Context; toSpeak = text; if (speaker == null) { speaker = new TextToSpeech(ctx, this); } else { var p = new Dictionary<string, string>(); speaker.Speak(toSpeak, QueueMode.Flush, p); } } public void OnInit([GeneratedEnum] OperationResult status) { if (status.Equals(OperationResult.Success)) { System.Diagnostics.Debug.WriteLine("speaker init"); var p = new Dictionary<string, string>(); speaker.Speak(toSpeak, QueueMode.Flush, p); } else { System.Diagnostics.Debug.WriteLine("was quiet"); } } } }
iOS平台實現ITextToSpeech接口:
最重要的[assembly: Dependency(typeof(TextToSpeech_iOS))] 這句注冊Dependency屬性。
using AVFoundation; using TextToSpeechDemo.iOS; using Xamarin.Forms; [assembly: Dependency(typeof(TextToSpeech_iOS))] namespace TextToSpeechDemo.iOS { class TextToSpeech_iOS : ITextToSpeech { public void Speak(string text) { var speechSynthesizer = new AVSpeechSynthesizer(); var speechUtterance = new AVSpeechUtterance(text) { Rate = AVSpeechUtterance.MaximumSpeechRate / 4, Voice = AVSpeechSynthesisVoice.FromLanguage("en-US"), Volume = 0.5f, PitchMultiplier = 1.0f }; speechSynthesizer.SpeakUtterance(speechUtterance); } } }
UWP平台實現ITextToSpeech接口:
最重要的[assembly: Dependency(typeof(TextToSpeech_UWP))] 這句注冊Dependency屬性。
using System; using TextToSpeechDemo.UWP; using Windows.Media.SpeechSynthesis; using Windows.UI.Xaml.Controls; using Xamarin.Forms; [assembly: Dependency(typeof(TextToSpeech_UWP))] namespace TextToSpeechDemo.UWP { class TextToSpeech_UWP : ITextToSpeech { public async void Speak(string text) { MediaElement mediaElement = new MediaElement(); var synth = new SpeechSynthesizer(); var stream = await synth.SynthesizeTextToStreamAsync(text); mediaElement.SetSource(stream, stream.ContentType); mediaElement.Play(); } } }
調用平台特性的時候通過DependencyService.Get<T>()實現:
public void btnSpeak_Clicked(object sender, EventArgs args) { DependencyService.Get<ITextToSpeech>().Speak(txtData.Text.Trim()); }
整體效果:
IPlatformInitializer
1、簡介
IPlatformInitializer其實為Prism.Forms共通類庫里面的一個接口,代碼如下:
namespace Prism { public interface IPlatformInitializer<T> { void RegisterTypes(T container); } }
包含一個注冊類型函數(注冊實現了平台特性的類型)。至於為什么是泛型接口?這是為了支持多種IOC容器(AutoFac,Unity,DryIoc,Ninject等),主流為Unity。Unity的IPlatformInitializer代碼如下:傳入了Unity的容器類型IUnityContainer
using Microsoft.Practices.Unity; namespace Prism.Unity { public interface IPlatformInitializer : IPlatformInitializer<IUnityContainer> { } }
2、工作原理
- 接口:定義功能接口在PCL類庫或者共享類庫
- 接口實現:各個平台實現接口功能
- 注冊:各個平台實現IPlatformInitializer接口,並在RegisterTypes方法中將實現接口的類注冊到IOC容器內
- 調用:ViewModel的構造函數添加接口為參數(Prism.Forms會自動從IOC容器加載)
調用RegisterTypes是在Prism.Forms共通類庫里面PrismApplicationBase<T>的構造函數中:
protected PrismApplicationBase(IPlatformInitializer<T> initializer = null) { base.ModalPopping += PrismApplicationBase_ModalPopping; base.ModalPopped += PrismApplicationBase_ModalPopped; _platformInitializer = initializer; InitializeInternal(); } /// <summary> /// Run the intialization process. /// </summary> void InitializeInternal() { ConfigureViewModelLocator(); Initialize(); OnInitialized(); } /// <summary> /// Run the bootstrapper process. /// </summary> public virtual void Initialize() { Logger = CreateLogger(); ModuleCatalog = CreateModuleCatalog(); ConfigureModuleCatalog(); Container = CreateContainer(); ConfigureContainer(); NavigationService = CreateNavigationService(); RegisterTypes(); _platformInitializer?
.RegisterTypes(Container);
InitializeModules();
}
3,實例使用
使用PrismTextToSpeech(文本語音)實例講解如何使用IPlatformInitializer。
項目結構:
接口定義:
namespacePrismTextToSpeech.Services
{ public interface ITextToSpeech { void Speak(string text); } }
Android平台實現ITextToSpeech接口:API定義
與DependencyService的區別是沒有Dependency屬性。
using Android.Runtime; using Android.Speech.Tts; using PrismTextToSpeech.Services; using System.Collections.Generic; using Xamarin.Forms; namespace PrismTextToSpeech.Droid { public class TextToSpeech_Android : Java.Lang.Object, ITextToSpeech, TextToSpeech.IOnInitListener { TextToSpeech speaker; string toSpeak; public TextToSpeech_Android() { } public void Speak(string text) { var ctx = Forms.Context; toSpeak = text; if (speaker == null) { speaker = new TextToSpeech(ctx, this); } else { var p = new Dictionary<string, string>(); speaker.Speak(toSpeak, QueueMode.Flush, p); } } public void OnInit([GeneratedEnum] OperationResult status) { if (status.Equals(OperationResult.Success)) { System.Diagnostics.Debug.WriteLine("speaker init"); var p = new Dictionary<string, string>(); speaker.Speak(toSpeak, QueueMode.Flush, p); } else { System.Diagnostics.Debug.WriteLine("was quiet"); } } } }
注冊類型到IOC容器:
using Android.App; using Android.Content.PM; using Android.OS; using Microsoft.Practices.Unity; using Prism.Unity; using PrismTextToSpeech.Services; namespace PrismTextToSpeech.Droid { [Activity(Label = "PrismTextToSpeech", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { protected override void OnCreate(Bundle bundle) { TabLayoutResource = Resource.Layout.tabs; ToolbarResource = Resource.Layout.toolbar; base.OnCreate(bundle); global::Xamarin.Forms.Forms.Init(this, bundle); LoadApplication(new App(new AndroidInitializer())); } } public class AndroidInitializer : IPlatformInitializer { public void RegisterTypes(IUnityContainer container) { container.RegisterType<ITextToSpeech, TextToSpeech_Android>
();
}
}
}
iOS與UWP的接口實現與DependencyService的一樣,唯獨就是沒有Dependency屬性,這里略過。
調用的時候:
using Prism.Commands; using Prism.Mvvm; using PrismTextToSpeech.Services; namespace PrismTextToSpeech.ViewModels { public class MainPageViewModel : BindableBase { private ITextToSpeech _textToSpeech; private string _speakText; public string SpeakText { get { return _speakText; } set { SetProperty(ref _speakText, value); SpeakCommand.RaiseCanExecuteChanged(); } } public MainPageViewModel(ITextToSpeech textToSpeech) { _textToSpeech=
textToSpeech; } public DelegateCommand SpeakCommand => new DelegateCommand( () => { _textToSpeech.Speak(SpeakText); }, () => !string.IsNullOrEmpty(SpeakText)).ObservesProperty(() => this.SpeakText); } }
Prism就是這么簡單,效果更佳:
DependencyAttribute+IPlatformInitializer
1、簡介
這種方式是Prism為了兼容DepdencyService而創建的,及Prism內部封裝了DependencyService。
namespace Prism.Services { /// <summary> /// A service that provides acess to platform-specific implementations of a specified type /// </summary> public class DependencyService : IDependencyService { /// <summary> /// Returns a platform-specific implementation of a type registered with the Xamarin.Forms.DependencyService /// </summary> /// <typeparam name="T">The type of class to get</typeparam> /// <returns>The class instance</returns> public T Get<T>() where T : class { return Xamarin.Forms.DependencyService.Get<T>(); } } }
2、使用方法
- 接口:與DependencyService或者IPlatformInitializer實例一樣
- 接口實現:與DependencyService實例一樣
- 注冊:與DependencyService實例一樣,各個平台實現接口的類庫注冊DependencyAttribute屬性
- 調用:與IPlatformInitializer實例一樣,ViewModel的構造函數添加接口為參數(Prism.Forms會自動從IOC容器加載)
總結
DependencyService其實就是依賴注入的自我實現,而Prism的IPlatformInitializer則巧妙的借助Unity等容器達到同樣的目的。不過從應用以后擴展角度也好,性能角度也好還是建議使用IOC容器技術(Prism創始人Brian Lagunas也是這么建議的)。特別是在使用Mvvm模式開發的時候,更加需要依賴IOC容器來管理ViewModel與Service,這也是我選擇Prism做Xamarin開發的原因之一。