Модульне додаток на Xamarin

У цій статті ви дізнаєтеся про цікаві проблеми і їхні рішення, які виникали в процесі розробки «конструктора» додатків, побудованого на модульній архітектурі, у компанії Notissimus. Проект знаходиться в активній стадії розробки, тому будемо раді дізнатися вашу думку в коментарях, а також запрошуємо на заключний у 2016 році митап для розробників на Xamarin. Всіх зацікавлених просимо під кат.

Далі розповідь буде вестися від імені авторів.
Постановка завдання
Що хоче клієнт?
Клієнт істота примхлива, тому вимоги до кінцевого продукту (з додатком) у кожного будуть свої. Однак, можна виділити і загальні хотілки:
  • налаштовувати функціональність під себе;
  • редагувати або повністю замінювати дизайн;
  • володіти вихідним кодом;
  • мати можливість продовжити розробку у своїй/іншій команді.
Ці чотири хотілки ніяк не змінюються від клієнта до клієнта, але можуть з'являтися/пропадати. Після визначення хотілок треба зрозуміти що ж вони означають для простого програміста:
  • модульність – необхідний певний базовий проект з доповненнями у вигляді модулів;
  • гнучкість налаштування – повинна бути можливість перевизначити бізнес-логіку і UI модулі;
  • ліцензування та захист вихідного коду – має бути обов'язково, так як планується передача вихідних на бік.
Схема вирішення
Визначившись зі своїми завданнями, ми вирішили використати наступну схему:
схема рішення
Архітектура рішення
Базові модулі
Що представляють собою базові модулі? По-перше, це якась архітектурна одиниця, що складається з трьох основних елементів: API, Core і UI. По-друге, це структура, повністю незалежна ні від чого, крім фундаментального Base проекту, в якому зібрані всі напрацювання і базові елементи для швидкої збірки і підключення нових модулів (наприклад, проект для спрощення роботи з API, *LookupService'и, обгортка над БД, базові ViewModel'і, базові класи для UIViewController'ів та інше). Таким чином, в основі кожного модуля лежить та або інша частина або частини фундаментального Base модуля.
Прикладами базових модулів є:
  • модуль авторизації і відображення інформації про користувача;
  • модуль чату;
  • модуль обраного;
  • модуль контактів;
  • модуль навігації*;
  • ...
Модуль навігації з * тому що він не є базовим модулем в чистому вигляді, так як від обраного типу навігації (меню, або вкладки, або щось ще) сильно залежить логіка обробки цієї навігації на UI шарі і також залежить точка входу в програму – стартова ViewModel, з якої починається запуск програми.
Модулі верхнього рівня
Це ті модулі, які залежать від сегмента бізнесу під який ведеться розробка проекту. Причини, по яких було прийнято рішення про виділення їх в окремий шар, очевидні, але ми все ж перерахуємо їх:
  • модулі базового шару розвантажуються і стають дійсно універсальними – не доводиться ставити різні милиці для обов'язково вимагає взаємодії між модулями;
  • з'являється можливість посилатися з одного модуля на інший, так як ці модулі належать одному сегменту, всередині нього вони повністю універсальні і можуть переиспользоваться;
  • отримуємо можливість написати лише ту логіку, яка потрібна саме цього сегмента – розвантажуємо модулі та додаток: зберігаємо невеликий розмір і швидкість роботи.
Прикладами таких модулів є:
  • модуль каталогу;
  • модуль кошика і оформлення замовлення;
  • модуль акцій та новин;
  • модуль адреси магазинів;
  • ...
З модуля каталогу необхідно додавати товари в кошик і реалізацію цього через додаткові обгортки без прямого посилання на модуль кошика можна назвати зручним способом.
Запускається проект
Це той проект, з якими можна взаємодіяти клієнту або його розробнику. Він містить:
  • посилання на всі підключені модулі і потрібні для роботи пакети;
  • набір графіки, що використовується в дизайні програми;
  • набір шрифтів;
  • колірна палітра;
  • набір текстів з локалізацією;
  • набір клієнтських налаштувань.
Що може зробити з цим проектом звичайний користувач, керуючись специфікацією:
  • поміняти іконки/картинки;
  • змінити шрифти;
  • змінити тексти;
  • поміняти кольори.
Що може зробити з цим проектом розробник:
  • теж що і користувач;
  • змінити налаштування в конфігах;
  • підмінити будь-яку частину логіки в підключених модулях (наприклад, змінити тип навігації);
  • додати додаткові модулі.
Архітектура модуля
API
Це Portable Class Library – бібліотека (проект) код якої може виконуватися на будь-якій платформі будь то iOS або Android. Стандартний API проект містить у собі такі елементи, як:
  • Model'і, одержувані від сервера і використовуються в Core;
  • Service'и, усередині яких відбувається виклик тих чи інших методів 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 { e }));
}

public Task SignOut(string token)
{
return Post(SIGN_OUT_URL, ToStringContent(new { token }));
}

#endregion
}

Після додавання сервісу в проект додаткових дій для його реєстрації не потрібно, «реєстратор» все робить сам завдяки такими рядками:
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();

Core
Це також PCL проект, повністю побудований на використанні можливостей, які нам надає MvvmCross. Стандартний Core проект містить наступні елементи:
  • набір ViewModel'їй – абстракції екранів, в яких можуть бути лише: реалізації інтерфейсу ICommand, прості властивості і методи навігації;
  • VmService'и – сервіси, зав'язані на конкретні ViewModel'і містять у собі всю бізнес-логіку. Кожен такий сервіс виконує строго одну функцію, наприклад:
public interface IMenuVmService
{
public IEnumerable BuildItemsFromJsonConfig();
}

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

  • Model'і – додаткові моделі, використовувані в Core і іноді в UI (приклад – моделі, що використовуються при побудові діалогів і повідомлень для користувача). Частіше всього моделі Core – це набір об'єктів, з якими працює БД;
  • Service'и – специфічні сервіси, як правило це або сервіси для роботи з БД, або лише оголошені інтерфейси сервісів (реалізація у такого сервісу платформений, приклад –
    IDeviceService
    , який отримує інформацію про поточний пристрої);
  • Message'и – повідомлення, що використовуються для взаємодії між непов'язаними частинами Core (наприклад, для сповіщення однієї ViewModel'і про дії в інший) або для передачі в Core параметрів UI шару і навпаки.
Перед початком розробки ми обговорили, що більша частина логіки в Core може бути перевизначено і кожна може бути замінена повністю вашої реалізацією. І якщо з заміною Service'ів через IoC все ясно, то з заміною ViewModel'їй не все очевидно. Постало питання: «як це реалізувати?». Відповіддю стала реалізація
ViewModelLookupService
.
ViewModelLookupService
Це сервіс, який дозволяє по інтерфейсу ViewModel'і реєструвати свою реалізацію. Принцип схожий на IoC, тільки ViewModelLookupService не працює з примірниками VM'ок. Як же тоді відбувається навігація? Справа в тому, що метод VM ShowViewModel() приймає в себе тип VM, яку потрібно відобразити. Таким чином, при реєстрації в'ю моделі в сервісі береться повна інформація про тип інтерфейсу VM і тип реалізації VM і зберігається в сервісі. При зверненні до сервісу для отримання зареєстрованої реалізації, він звертається до збережених даних і назад повертає тип реалізації.
Це нам дає можливість задавати свої реалізації моделей в конфігах. Приклад:
Конфігурування елементів списку в модулі Меню
...
"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
. при натисканні на елемент і виклику логіки переходів, а також задати наявність бейджа у пункту і визначити один з пунктів як стартовий екран.
Завдяки введенню
ViewModelLookupService
всі VM'ки обзавелися власним інтерфейсом – це дозволяє також не втрачати можливість заміни логіки при биндинге VM на UI шарі. Також реєстрація реалізацій своїх ViewModel'їй в
ViewModelLookupService
є обов'язковою умовою для кожного модуля.
RouterService
насправді з навігацією модуля через Меню
ViewModelLookupService
не все так просто. Після реалізації цього механізму ми подумали, що у модуля навігації не повинно бути явною прив'язки до навигируемому типу, а також повинна бути можливість виконати деяку логіку перед вчиненням навігації в пункт меню (наприклад, в меню може бути пункт Особистий кабінет або Історія замовлень, доступ в які повинен бути заблокований до авторизації користувача). Тому було вирішено розробити механізм RouterService'а.
RouterService
– це сервіс, який управляє навігацією за типом інтерфейсу VM'ки. Виклик його відбувається наступним чином:
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}");
}

Для обробки події навігації на який-небудь тип модуля необхідно зареєструвати на цей тип в RouterService'е свою реалізацію
IRouterSubscriber
, який містить у собі лише два методу:
public interface IRouterSubscriber
{
bool CanNavigatedTo(string type);

void FailedNavigatedTo(string type);
}

Перший викликається всередині
RouterService.NavigatedTo(...)
методі, якщо за типом
item.Type
був зареєстрований користувач. Другий, якщо перший метод повернув false або виникла якась помилка на інших етапах навігації.
При реалізації першого методу передплатник зобов'язаний обробити прийшов йому тип, виконати необхідні перевірки та в разі їх проходження отримати від
ViewModelLookupService
зареєстрований тип реалізації моделі і виконати на нього навігацію, інакше необхідно повернути
false
. При реалізації
FailedNavigatedTo(...)
ніяких обмежень немає.
Таким чином, обробка навігації на ключові точки була винесена з модуля Меню і дозволила виконувати навігацію на будь-які ViewModel'і і виконувати будь-яку логіку (наприклад, при натисканні на пункт меню потрібно виконати навігацію не на екран, а відкрити сайт компанії)
UI
Шар складається з проектів двох типів:
  • iOS Class Library;
  • Android Class Library.
Кожен з проектів обов'язково містить у собі:
  • реалізацію інтерфейсів платформних сервісів;
  • інтерфейси – екрани, побудовані за абстракцій – ViewModel'ям.
Реалізацію платформних сервісів ми подивимося трохи пізніше, реалізація користувацьких інтерфейсах не відрізняється від тієї, що ви робите зараз, тому розберемося докладніше у використанні різних клієнтських налаштувань програми.
Установки бувають двох типів:
  • кофигурирующие – впливають на роботу Core і логіку взаємодії всередині і між модулями (приклад наведений вище конфіг модуля Меню);
  • тематичні – впливають на малювання різних компонентів у UI шарі модулів.
Сам по собі файл налаштувань – це .json документ. Налаштування завантажуються один раз на спеціальні сервіси, що стартують при запуску модуля. Конфигурирующие налаштування завантажуються в Core ConfigService'и, тематичні – UI в ThemeServices. Процедура завантаження json'а з файлу досить стандартна, за винятком того, що Core – PCL, тобто інструменти роботи з файлами там відсутні (див. .NET Standard 2.0). Це призвело до впровадження спеціального сервісу
ISettingsService
, реалізація якого знаходиться в UI шарі фундаментального Base модуля, що дозволяє виконувати логіку завантаження інформації про настройках без проблем.
Етапи розробки нового модуля і підключення його до існуючої системи
Перед розробкою нового модуля необхідно буде придбати та завантажити з особистого кабінету клієнта його вихідні коди програми. Таким чином, у вас виявиться рішення з двома запускаються проектами (під iOS і під Android) з вже створеної архітектурою і вибраними параметрами. Зараз буде розглядатися лише створення модуля фотогалереї з нуля для існуючого iOS додатки. Модуль буде отримувати знімки з камери пристрою, відправляти їх на сервер, зберігати в альбом, і відображати в колекції.
Створення архітектури
Спершу для зручності створюємо нову Solution Folder, називаємо її Photogallery. Після цього послідовно додаємо в цю папку три проекти:
  • Portable Library – Photogallery.API;
  • Portable Library – Photogallery.Core;
  • iOS Class Library – Photogallery.iOS.
Видаляємо автоматично створені
MyClass.cs
і додаємо в проекти наступні посилання:
  • Photogallery.API – Base.API;
  • Photogallery.Core – Base.Core + Photogallery.API;
  • Photogallery.iOS – Base.iOS + Base.Core + Base.API + Photogallery.Core + Photogallery.API;
  • XamarinMeetUp.iOS – Base.iOS + Base.Core + Base.API + Photogallery.iOS + Photogallery.Core + Photogallery.API.
Також необхідно до кожного проектів підключити MvvmCross пакет з NuGet.
Додавання сервісу 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
}

Завдяки реалізації
BaseService
Base.API проекті Base модуля, виконання запиту по необхідному URL виконується всього в один рядок. Аналогічним чином можна додати реалізацію методу отримання фотографій від сервера. Точка входу API береться з налаштувань в запускаемом проекті і використовується як префікс URL у всіх запитів. Якщо з якоїсь причини реалізація Post(...) методу не влаштовує, можна звернутися безпосередньо до сервісу запитів.
Щоб сервіс заробив, залишилося зареєструвати його. Для цього створимо в API проекті клас App і напишемо в нього наступний код:
public class App : MvxApplication
{
public override void Initialize()
{
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
}
}

Тут у методі
Initialize
ми автоматично реєструємо всі сервіси в API як Lazy синглтоны для їх подальшого виклику Core частини.
Створення ViewModel'та її Service'а
Для даного модуля ми зробимо просту VM, яка буде містити лише перелік отриманих від користувача фотографій і кнопку додавання нової фотографії. У проекті Core створюємо папку ViewModels, всередині неї папку Photogallery і туди додаємо новий інтерфейс
IPhotogalleryViewModel
і новий клас
PhotogalleryViewModel
, який успадковуємо від інтерфейсу і від
BaseViewModel
.
В інтерфейс IPhotogalleryViewModel додамо наступні рядки:
ObservableCollection<IPhotoItemVM> Items { get; set; }

ICommand AddPhotoCommand { get; }

Items – список відображуваних фотографій, AddPhotoCommand – додавання нової фотографії у колекцію.
Завантаження всіх фотографій і логіка отримання нової фотографії буде в сервісі, що реалізує інтерфейс:
public interface IPhotogalleryVmService
{
Task<ObservableCollection<IPhotoItemVM>> LoadItems();

Task<IPhotoItemVM> GetPhotoFromUser();
}

VmService
для отримання нової фотографії звертатися до сервісу камери пристрою, реалізація якого буде на кожній платформі своя, і, для завантаження фотографій з альбому, до сервісу роботи з альбомами.
Інтерфейси платформних сервісів
public interface ICameraService
{
Task<byte[]> TakePhoto();
}

public interface IPhotoAlbumService
{
Task<List<byte[]>> LoadPhotosFrom(string albumName);
}

Залишилося лише зареєструвати наявні в Core сервіси і ViewModel'і (реєстрація вьюмоделей відбувається для можливості їх подальшої заміни). Відбувається все за аналогією з 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;
}

Не забуваємо додати Info.plist ключі
NSCameraUsageDescription
та
NSPhotoLibraryUsageDescription
.
Для верстки екрану додамо в проект папку View, в ній створимо папку Photogallery і в неї додамо
PhotogalleryViewController
. Додамо в Interface Builder на
PhotogalleryViewController
два елемента –
UICollectionView
та
UIButton
і створимо для них аутлети
_photoCollection
та
_addPhotoBtn
відповідно. Тепер сбиндим їх у методі
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();
}

Зараз наш модуль повністю готовий до роботи, залишилося лише підключити його до основного проекту.
Підключення нового модуля до основного проекту
Для підключення нашого модуля необхідно виконати шість кроків:
Перший. Додати в Core проект клас
PluginLoader
, який буде запускати ініціалізацію App.cs.
PluginLoader
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;
}
}

Другий. Додати в UI проект клас Plugin, в якому буде реєструватися ViewController і платформні електронні сервіси.
Plugin
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>
{
}

Четвертий. Прописати навігацію на фотогалерею з меню в конфіги.
Навігація на фотогалерею
{
"icon":"res:Images/Menu/photo.png", 
"name":"Фотогалерея", 
"type":"Photogallery.Core.ViewModels.Photogallery.IPhotogalleryViewModel" 
}

П'ятий. Додати обробку події навігації Core плагіна.
PhotogalleryRouterSubscriber
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
}
}

Шостий. І зареєструвати його в App.cs.
Реєстрація PhotogalleryRouterSubscriber
var routerService = Mvx.Resolve<IRouterService>();

routerService.Register<IPhotogalleryViewModel>(new PhotogalleryRouterSubscriber());

Запустимо наш проект і переконаємося, що все працює як запланували.
Висновок
Ми розглянули основні моменти при роботі з нашою платформою. Головні думки, які хотілося донести:
  • Ви нічим не обмежені;
  • Спробуйте MvvmCross;
  • Будьте новаторами.
Обговорення з'явилися в процесі читання думок пропонуємо перенести в коментарі. Дякую, що прочитали!
Про авторів

Максим Євтух – Розробник мобільних додатків на фреймворку Xamarin в компанії «НОТИССИМУС». В розробці мобільного з 2013 року. У вільний час займається вивченням питання удосконалення MvvmCross'а і підтримкою контрола GitHub для реалізації нових гайдів Material Design.

Денис Кретов – технічний директор в компанії «НОТИССИМУС». Спеціалізується на розробці мобільних додатків для інтернет-магазинів, а також рішень на базі iBeacon.
Інші статті з нашого блогу про Xamarin читайте за посиланням #xamarincolumn.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.