Xamarinモジュラーアプリケーション

この記事では、 Notissimusのモジュラーアーキテクチャ上に構築されたアプリケーションの「デザイナー」の開発中に生じた興味深い問題とその解決策について学びます。 プロジェクトは活発に開発されているので、コメントであなたの意見を知ってうれしく思います。また、Xamarinの開発者向けの2016年の最終会議にも招待します。 猫の下で興味を持っているみんなにお願いします。









さらに、ナレーションは著者に代わって実施されます。







問題の声明



クライアントは何を望んでいますか?



クライアントは気まぐれな生き物なので、誰もが最終製品(アプリケーション)に対して独自の要件を持っています。 ただし、一般的なウィッシュリストは区別できます。









これらの4つのウィッシュリストはクライアントごとに変わりませんが、表示/非表示になる場合があります。 ウィッシュリストを決定したら、単純なプログラマーにとってそれらが何を意味するかを理解する必要があります。









ソリューションスキーム



タスクを決定したら、次のスキームを使用することにしました。







ソリューションスキスム







ソリューションのアーキテクチャ



基本モジュール



基本モジュールとは何ですか? まず、API、コア、UIの3つの主要な要素で構成される一種のアーキテクチャユニットです。 第二に、新しいモジュールの迅速なアセンブリと接続のためのすべてのベストプラクティスと基本要素を含む基本的なベースプロジェクト(たとえば、API、LookupService、ラッパーオーバーDB、ベースViewModel、UIViewControllersのベースクラスなど)。 したがって、各モジュールの基礎は、基本的なBaseモジュールの1つまたは複数の部分です。







基本モジュールの例は次のとおりです。









UI層でこのナビゲーションを処理するロジックは、選択したナビゲーションのタイプ(メニュー、タブ、またはその他)に大きく依存し、アプリケーションへのエントリポイントも依存するため、ナビゲーションモジュールは*を使用します。 ViewModelを起動します。これにより、アプリケーションが起動します。







トップレベルのモジュール



これらは、プロジェクトが開発されているビジネスセグメントに依存するモジュールです。 それらを個別のレイヤーに分離することが決定された理由は明らかですが、私たちはまだそれらをリストしています:









そのようなモジュールの例は次のとおりです。









カタログモジュールからバスケットに商品を追加する必要があり、バスケットモジュールに直接リンクせずに追加のラッパーを使用してこれを実装することは、便利な方法とは言えません。







プロジェクトを開始



これは、クライアントまたは開発者が対話できるプロジェクトです。 次のものが含まれます。









一般のユーザーがこのプロジェクトでできることは、仕様にガイドされています:









このプロジェクトで開発者ができること:









モジュールのアーキテクチャ



API



これはポータブルクラスライブラリです。iOSまたはAndroidを問わず、任意のプラットフォームで実行できるライブラリ(プロジェクト)コードです。 標準APIプロジェクトには、次のような要素が含まれます。









サービスは次のとおりです
public interface IAuthService { /// <summary> ///    e-mail   /// </summary> /// <returns>  </returns> /// <param name="email">E-mail</param> /// <param name="password"></param> Task<string> SignIn(string email, string password); /// <summary> ///    e-mail   .  /// </summary> /// <returns>  </returns> /// <param name="email">E-mail</param> /// <param name="socialTypeName">  . </param> /// <param name="additionalFields"> </param> Task<string> SignInSocial(string email, string socialTypeName, Dictionary<string, object> additionalFields = null); /// <summary> ///    e-mail   /// </summary> /// <returns>  </returns> /// <param name="email">E-mail</param> /// <param name="password"></param> /// <param name="additionalFields"> </param> Task<string> SignUp(string email, string password, Dictionary<string, object> additionalFields = null); /// <summary> ///    /// </summary> /// <returns>  </returns> /// <param name="email">E-mail</param> Task<string> RecoveryPassword(string email); /// <summary> ///   /// </summary> /// <param name="token">  </param> Task SignOut(string token); } public class AuthService : BaseService, IAuthService { #region IAuthService implementation public async Task<string> SignIn(string email, string password) { return await Post<string>(SIGN_IN_URL, ToStringContent(new { email, password })); } public async Task<string> SignInSocial(string email, string socialTypeName, Dictionary<string, object> additionalFields = null) { return await Post<string>(SIGN_IN_SOCIAL_URL, ToStringContent(new { email, socialTypeName, additionalFields })); } public async Task<string> SignUp(string email, string password, Dictionary<string, object> additionalFields = null) { return await Post<string>(SIGN_UP_URL, ToStringContent(new { email, password, additionalFields })); } public async Task<string> RecoveryPassword(string email) { return await Post<string>(RECOVERY_PASSWORD_URL, ToStringContent(new { email })); } public Task SignOut(string token) { return Post(SIGN_OUT_URL, ToStringContent(new { token })); } #endregion }
      
      





プロジェクトにサービスを追加した後、登録するための追加手順は必要ありません;「レジストラ」は、次の行のおかげですべて自分で行います。







 CreatableTypes() .EndingWith("Service") .AsInterfaces() .RegisterAsLazySingleton();
      
      





コア



これはPCLプロジェクトでもあり、MvvmCrossが提供する機会を使用して完全に構築されています。 標準のコアプロジェクトには、次の要素が含まれます。









 public interface IMenuVmService { public IEnumerable BuildItemsFromJsonConfig(); } public class MenuVmService : IMenuVmService { public IEnumerable BuildItemsFromJsonConfig() { ... } }
      
      







開発を開始する前に、Coreのロジックのほとんどを再定義でき、実装ごとに完全に置き換えることができることを説明しました。 また、IoCを介したサービスの置き換えですべてが明らかな場合、ViewModelsの置き換えですべてが明らかではありません。 「これを実装する方法は?」という疑問が生じました。 答えはViewModelLookupService



の実装でした。







ViewModelLookupService



これは、ViewModelインターフェースを介して実装を登録できるサービスです。 原則はIoCに似ており、VMインスタンスではViewModelLookupServiceのみが機能しません。 ナビゲーションはどうですか? 実際、ShowViewModel()VMメソッドは、表示するVMのタイプを取り込みます。 したがって、ビューモデルをサービスに登録すると、VMインターフェイスの種類とVM実装の種類に関する完全な情報が取得され、サービスに保存されます。 登録された実装を取得するためにサービスにアクセスするとき、保存されたデータにアクセスし、実装のタイプを返します。







これにより、configsでモデルの実装を設定できます。 例:







メニューモジュールでリストアイテムを構成する
 ... "items": [ { "icon":"res:Images/Menu/catalog.png", "name":"", "type":"AppRopio.ECommerce.Products.Core.ViewModels.IProductsViewModel", "default":true }, { "icon":"res:Images/Menu/basket.png", "name":"", "type":"AppRopio.ECommerce.Basket.Core.ViewModels.IBasketViewModel", "badge":true }, { "icon":"res:Images/Menu/history.png", "name":" ", "type":"AppRopio.ECommerce.OrdersHistory.Core.ViewModels.IOrdersHistoryViewModel" }, { "icon":"res:Images/Menu/favorites.png", "name":"", "type":"AppRopio.ECommerce.Favorites.Core.ViewModels.IFavoritesViewModel" } ] ...
      
      





したがって、リストアイテムを設定できます:名前、VMタイプ、アイテムをクリックしてナビゲーションロジックを呼び出すときにViewModelLookupService



から取得しようとし、アイテムのバッジを設定し、アイテムの1つを開始画面として指定する必要があります。







ViewModelLookupService



の導入のおかげでViewModelLookupService



すべてのVMには独自のインターフェイスがあります。これにより、UIレイヤーでVMをバインドするときにロジックを置き換える機能を失うこともなくなります。 また、ViewModelLookupServiceでのViewModelLookupService



の実装の登録は、各モジュールの前提条件です。







RouterService



実際、 ViewModelLookupService



を介してMenuモジュールから移動するのはそれほど簡単でViewModelLookupService



ません。 このメカニズムを実装した後、ナビゲーションモジュールには、ナビゲートされる型への明示的なバインドがなく、メニュー項目にナビゲートする前にいくつかのロジックを実行できるようにする必要があると考えました(たとえば、メニューには個人アカウントまたは注文履歴、ユーザー認証の前にブロックする必要があります)。 したがって、RouterServiceメカニズムを開発することが決定されました。







RouterService



は、VMインターフェイスタイプRouterService



ナビゲーションを制御するサービスです。 彼の呼びかけは次のとおりです。







 protected void OnItemSelected(IMenuItemVM item) { if (!RouterService.NavigatedTo(item.Type)) MvxTrace.Trace(MvvmCross.Platform.Platform.MvxTraceLevel.Error, "NavigationError: ", $"Can't navigate to ViewModel of type {item.Type}"); }
      
      





任意のタイプのナビゲーションイベントを処理するには、モジュールは、このタイプのIRouterSubscriber



実装をIRouterSubscriber



に登録する必要があります。これには、2つのメソッドのみが含まれます。







 public interface IRouterSubscriber { bool CanNavigatedTo(string type); void FailedNavigatedTo(string type); }
      
      





サブスクライバーがitem.Type



型で登録されている場合、最初のメソッドはRouterService.NavigatedTo(...)



メソッド内でRouterService.NavigatedTo(...)



れます。 次に、最初のメソッドがfalseを返した場合、またはナビゲーションの他の段階でエラーが発生した場合。







最初のメソッドを実装するとき、サブスクライバーは自分に来た型を処理し、必要なチェックを実行し、合格した場合、 ViewModelLookupService



から登録済みのモデル実装型を取得してナビゲートする必要があります。そうでない場合はfalse



必要があります。 FailedNavigatedTo(...)



実装する場合、制限はありません。







したがって、キーポイントへのナビゲーションの処理はMenuモジュールから取り出され、任意のViewModelsおよび任意のロジックへのナビゲーションが許可されました(たとえば、メニュー項目をタップする場合、画面ではなく会社のWebサイトでのナビゲーションが必要です)







UI



レイヤーは、2種類のプロジェクトで構成されます。









各プロジェクトには必ず次のものが含まれます。









プラットフォームサービスの実装については、後ほど説明します。ユーザーインターフェイスの実装は、現在実行しているものと変わらないので、アプリケーションのさまざまなクライアント設定の使用について詳しく調べます。







設定には次の2つのタイプがあります。









設定ファイル自体は.jsonドキュメントです。 設定は、モジュールの起動時に開始される特別なサービスに一度読み込まれます。 構成設定は、ConfigService'yのコア、テーマ別-ThemeServicesのUIにロードされます。 ファイルからjsonをロードする手順はかなり標準的です。ただし、CoreはPCLです。つまり、そこにファイルを操作するツールはありません(.NET Standard 2.0を参照)。 これにより、特別なサービスISettingsService



が導入され、その実装は基本的なBaseモジュールのUIレイヤーにあり、ロジックが問題なく構成情報をロードできるようになりました。







新しいモジュールを開発し、既存のシステムに接続する段階



新しいモジュールを開発する前に、クライアントの個人アカウントからアプリケーションのソースコードを購入してダウンロードする必要があります。 したがって、すでに作成されたアーキテクチャと選択された設定を使用して、2つの起動されたプロジェクト(iOSおよびAndroid用)を備えたソリューションが得られます。 次に、既存のiOSアプリケーション用にゼロからフォトギャラリーモジュールを作成することのみを検討します。 モジュールは、デバイスのカメラから画像を受信し、サーバーに送信し、アルバムに保存して、コレクションに表示します。







アーキテクチャの作成



まず、便宜上、新しいソリューションフォルダーを作成し、フォトギャラリーと呼びます。 その後、3つのプロジェクトがこのフォルダーに追加されます。









自動的に作成されたMyClass.cs



を削除し、次のリンクをプロジェクトに追加します。









NuGetからMvvmCrossパッケージを各プロジェクトに接続することも必要です。







APIサービスの追加



写真を撮るとき、プラグインは写真をサーバーに送信して履歴を保存します(たとえば、公開用)。 これを行うには、この作業を実行するAPIプロジェクトにサービスを追加します。 プロジェクトにServicesフォルダーを作成し、 IPhotoService



インターフェイスを追加します。ここで、必要な機能を説明します。







 public interface IPhotoService { Task SendPhoto(byte[] photoData); }
      
      





次に、サービスの実装を記述します。







 public class PhotoService : BaseService, IPhotoService { private const string PHOTO_URL = "photo"; #region IPhotoService implementation public async Task SendPhoto(byte[] photoData) { await Post(PHOTO_URL, new ByteArrayContent(photoData)); } #endregion }
      
      





BaseモジュールのBase.APIプロジェクトにBaseService



が実装されているため、必要なURLでのクエリは1行だけで実行されます。 同様に、サーバーから写真を撮る方法の実装を追加できます。 APIエントリポイントは、起動されたプロジェクトの設定から取得され、すべてのリクエストのURLプレフィックスとして使用されます。 何らかの理由でPost(...)の実装がメソッドに合わない場合は、クエリサービスに直接連絡できます。







サービスを機能させるには、登録する必要があります。 これを行うには、APIプロジェクトでAppクラスを作成し、次のコードを記述します。







 public class App : MvxApplication { public override void Initialize() { CreatableTypes() .EndingWith("Service") .AsInterfaces() .RegisterAsLazySingleton(); } }
      
      





ここでは、 Initialize



メソッドで、APIのすべてのサービスを、Coreパーツからの後続の呼び出しの遅延シングルトーンとして自動的に登録します。







ViewModelとそのサービスの作成



このモジュールでは、単純なVMを作成します。このVMには、ユーザーから受け取った写真のリストと、新しい写真を追加するボタンのみが含まれます。 Coreプロジェクトで、ViewModelsフォルダーをIPhotogalleryViewModel



フォルダー内に作成し、新しいIPhotogalleryViewModel



インターフェイスと新しいIPhotogalleryViewModel



クラスを追加しBaseViewModel



。これらは、インターフェイスとBaseViewModel



を継承しBaseViewModel









IPhotogalleryViewModelインターフェイスに次の行を追加します。







 ObservableCollection<IPhotoItemVM> Items { get; set; } ICommand AddPhotoCommand { get; }
      
      





アイテム-表示される写真のリスト、AddPhotoCommand-新しい写真をコレクションに追加します。







すべての写真と新しい写真を取得するためのロジックのダウンロードは、インターフェイスを実装するサービスにあります。







 public interface IPhotogalleryVmService { Task<ObservableCollection<IPhotoItemVM>> LoadItems(); Task<IPhotoItemVM> GetPhotoFromUser(); }
      
      





新しい写真VmService



取得VmService



ために、 VmService



はデバイスのカメラサービスにアクセスし、その実装は各プラットフォーム上にあり、アルバムから写真をアップロードするためにアルバムサービスにアクセスします。







プラットフォームサービスインターフェイス
 public interface ICameraService { Task<byte[]> TakePhoto(); } public interface IPhotoAlbumService { Task<List<byte[]>> LoadPhotosFrom(string albumName); }
      
      





CoreおよびViewModel'iで利用可能なサービスを登録することのみが残ります(ビューモデルの登録は、その後の置換のために可能です)。 すべてはAPIとの類推によって行われます。App.csが作成され、Initializeメソッドが次のように再定義されます。







 public override void Initialize() { (new API.App()).Initialize(); CreatableTypes() .EndingWith("Service") .AsInterfaces() .RegisterAsLazySingleton(); var vmLookupService = Mvx.Resolve<IViewModelLookupService>(); vmLookupService.Register<IPhotogalleryViewModel>(typeof(PhotogalleryViewModel)); }
      
      





iOSでのシンプルなレイアウトの開発とプラットフォームサービスの実装



まず、すべてのプラットフォームサービスを実装します。 カメラサービスから始めましょう。 iOSプロジェクトにServicesフォルダーを作成し、CameraServiceを追加します。







 public class CameraService : ICameraService { public Task<byte[]> TakePhoto() { throw new NotImplementedException(); } }
      
      





TakePhoto()メソッドの実装
 public async Task<byte[]> TakePhoto() { var mediaFile = await CrossMedia.Current.TakePhotoAsync( new StoreCameraMediaOptions { DefaultCamera = CameraDevice.Rear }); var stream = mediaFile.GetStream(); var bytes = new byte[stream.Length]; await stream.ReadAsync(bytes, 0, (int)stream.Length); PHAssetCollection assetCollection = null; var userCollection = PHAssetCollection.FetchAssetCollections(PHAssetCollectionType.Album, PHAssetCollectionSubtype.Any, null); if (userCollection != null) assetCollection = userCollection.FirstOrDefault(nsObject => (nsObject as PHAssetCollection).LocalizedTitle == ALBUM_NAME) as PHAssetCollection; if (assetCollection == null) { string assetCollectionIdentifier = string.Empty; PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() => { var creationRequest = PHAssetCollectionChangeRequest.CreateAssetCollection(ALBUM_NAME); assetCollectionIdentifier = creationRequest.PlaceholderForCreatedAssetCollection.LocalIdentifier; }, (bool success, NSError error) => { assetCollection = PHAssetCollection.FetchAssetCollections(new[] { assetCollectionIdentifier }, null).firstObject as PHAssetCollection; PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() => { var assetChangeRequest = PHAssetChangeRequest.FromImage(UIImage.LoadFromData(NSData.FromArray(bytes))); var assetCollectionChangeRequest = PHAssetCollectionChangeRequest.ChangeRequest(assetCollection); assetCollectionChangeRequest.AddAssets(new[] { assetChangeRequest.PlaceholderForCreatedAsset }); }, (bool s, NSError e) => { }); }); } else { PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() => { var assetChangeRequest = PHAssetChangeRequest.FromImage(UIImage.LoadFromData(NSData.FromArray(bytes))); var assetCollectionChangeRequest = PHAssetCollectionChangeRequest.ChangeRequest(assetCollection); assetCollectionChangeRequest.AddAssets(new[] { assetChangeRequest.PlaceholderForCreatedAsset }); }, (bool success, NSError error) => { }); } return bytes; }
      
      





フォトアルバムを操作するためのサービスも追加します。







 public class PhotoAlbumService : IPhotoAlbumService { public Task<List<byte[]>> LoadPhotosFrom(string albumName) { throw new NotImplementedException(); } }
      
      





LoadPhotosFromメソッドの実装(string albumName)
 public Task<List<byte[]>> LoadPhotosFrom(string albumName) { var photos = new List<byte[]>(); var tcs = new TaskCompletionSource<List<byte[]>>(); var userCollection = PHAssetCollection.FetchAssetCollections(PHAssetCollectionType.Album, PHAssetCollectionSubtype.Any, null); if (userCollection != null) { var meetUpAssetCollection = userCollection.FirstOrDefault(nsObject => (nsObject as PHAssetCollection).LocalizedTitle == "Xamarin MeetUp") as PHAssetCollection; if (meetUpAssetCollection != null) { var meetUpPhotoResult = PHAsset.FetchAssets(meetUpAssetCollection, null); if (meetUpPhotoResult.Count > 0) meetUpPhotoResult.Enumerate((NSObject element, nuint index, out bool stop) => { var asset = element as PHAsset; PHImageManager.DefaultManager.RequestImageData(asset, null, (data, dataUti, orientation, info) => { var bytes = data.ToArray(); photos.Add(bytes); if (index == (nuint)meetUpPhotoResult.Count - 1) tcs.TrySetResult(photos); }); stop = index == (nuint)meetUpPhotoResult.Count; }); else return new Task<List<byte[]>>(() => photos); } } else return new Task<List<byte[]>>(() => photos); return tcs.Task; }
      
      





NSCameraUsageDescription



およびNSPhotoLibraryUsageDescription



キーをInfo.plistに追加することを忘れないでください。







画面を構成するには、Viewフォルダーをプロジェクトに追加し、その中にPhotogalleryフォルダーを作成して、PhotogalleryViewControllerを追加PhotogalleryViewController



UICollectionView



のInterface Builderに2つの要素、 UICollectionView



UIButton



_addPhotoBtn



それぞれ_photoCollection



_addPhotoBtn



のアウトレットを作成しUIButton



。 次に、 BindControls



メソッドでそれらをバインドします。







 protected override void BindControls() { _photoCollection.RegisterNibForCell(PhotogalleryCell.Nib, PhotogalleryCell.Key); var dataSource = new MvxCollectionViewSource(_photoCollection, PhotogalleryCell.Key); var set = this.CreateBindingSet<PhotogalleryViewController, IPhotogalleryViewModel>(); set.Bind(dataSource).To(vm => vm.Items); set.Bind(_addPhotoBtn).To(vm => vm.AddPhotoCommand); set.Apply(); _photoCollection.DataSource = dataSource; _photoCollection.ReloadData(); }
      
      





これで、モジュールは完全に作業できる状態になりました。メインプロジェクトに接続するだけです。







新しいモジュールをメインプロジェクトに接続する



モジュールを接続するには、6つの手順を実行する必要があります。







最初のものPluginLoader



クラスをCoreプロジェクトに追加します。これにより、App.csの初期化が開始されます。







プラグインローダー
 public class PluginLoader : IMvxPluginLoader { public static readonly PluginLoader Instance = new PluginLoader(); private bool _loaded; public void EnsureLoaded() { if (_loaded) return; new App().Initialize(); var manager = Mvx.Resolve<IMvxPluginManager>(); manager.EnsurePlatformAdaptionLoaded<PluginLoader>(); MvxTrace.Trace("Auth plugin is loaded"); _loaded = true; } }
      
      





二番目 。 ViewControllerおよびプラットフォームサービスが登録されるUIプロジェクトにプラグインクラスを追加します。







プラグイン
 public class Plugin : IMvxPlugin { public void Load() { var viewLookupService = Mvx.Resolve<IViewLookupService>(); viewLookupService.Register<IPhotogalleryViewModel, PhotogalleryViewController>(); Mvx.RegisterSingleton<ICameraService>(() => new CameraService()); Mvx.RegisterSingleton<IPhotoAlbumService>(() => new PhotoAlbumService()); } }
      
      





第三 。 クラスXMU_PhotogalleryPluginBootstrap



を起動したプロジェクトに追加します。







XMU_PhotogalleryPluginBootstrap
 public class XMU_PhotogalleryPluginBootstrap : MvxLoaderPluginBootstrapAction<PluginLoader, Photogallery.iOS.Plugin> { }
      
      





4番目 。 構成のメニューからフォトギャラリーへのナビゲーションを指定します。







フォトギャラリーナビゲーション
 { "icon":"res:Images/Menu/photo.png", "name":"", "type":"Photogallery.Core.ViewModels.Photogallery.IPhotogalleryViewModel" }
      
      





5番目 。 ナビゲーションイベント処理をCoreプラグインに追加します。







フォトギャラリーRouterSubscriber
 public class PhotogalleryRouterSubscriber : MvxNavigatingObject, IRouterSubscriber { private string VM_TYPE = (typeof(IPhotogalleryViewModel)).FullName; public override bool CanNavigatedTo(string type) { return type == VM_TYPE ? ShowViewModel(LookupService.Resolve(type)) : false; } public override void FailedNavigatedTo(string type) { //nothing } }
      
      





6番目 。 それをApp.csに登録します。







登録PhotogalleryRouterSubscriber
 var routerService = Mvx.Resolve<IRouterService>(); routerService.Register<IPhotogalleryViewModel>(new PhotogalleryRouterSubscriber());
      
      





プロジェクトを実行し、すべてが計画どおりに機能することを確認します。







おわりに



プラットフォームで作業する際の主なポイントを調べました。 私が伝えたかった主な考え:









読書中に現れた思考の議論をコメントに移すことをお勧めします。 読んでくれてありがとう!







著者について



Maxim Evtukh -NOTISSIMUS社のXamarinフレームワークでのモバイルアプリケーションの開発者。 2013年以降のモバイル開発。 空き時間に、彼女はMvvmCrossを改善し、新しいマテリアルデザインガイドを実装するためのGitHubコントロールをサポートする問題を研究しています。





Denis Kretov-NOTISSIMUSのテクニカルディレクター。 彼は、iBeaconに基づいたソリューションだけでなく、オンラインストア向けのモバイルアプリケーションの開発を専門としています。







Xamarinブログのその他の記事については、# xamarincolumnをご覧ください。








All Articles