Модульное приложение на 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
{
///
/// Авторизация пользователя по e-mail и паролю
///
/// Авторизационный токен пользователя
/// E-mail
/// Пароль
Task SignIn(string email, string password);
///
/// Авторизация пользователя по e-mail и типу соц. сети
///
/// Авторизационный токен пользователя
/// E-mail
/// Название типа соц. сети
/// Дополнительные поля
Task SignInSocial(string email, string socialTypeName, Dictionary additionalFields = null);
///
/// Регистрация пользователю по e-mail и паролю
///
/// Авторизационный токен пользователя
/// E-mail
/// Пароль
/// Дополнительные поля
Task SignUp(string email, string password, Dictionary additionalFields = null);
///
/// Восстановление забытого пароля
///
/// Сообщение для пользователя
/// E-mail
Task RecoveryPassword(string email);
///
/// Завершение сессии
///
/// Авторизационный токен пользователя
Task SignOut(string token);
}
public class AuthService : BaseService, IAuthService
{
#region IAuthService implementation
public async Task SignIn(string email, string password)
{
return await Post(SIGN_IN_URL, ToStringContent(new { email, password }));
}
public async Task SignInSocial(string email, string socialTypeName, Dictionary additionalFields = null)
{
return await Post(SIGN_IN_SOCIAL_URL, ToStringContent(new { email, socialTypeName, additionalFields }));
}
public async Task SignUp(string email, string password, Dictionary additionalFields = null)
{
return await Post(SIGN_UP_URL, ToStringContent(new { email, password, additionalFields }));
}
public async Task RecoveryPassword(string email)
{
return await Post(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();
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 Items { get; set; }
ICommand AddPhotoCommand { get; }
Items — список отображаемых фотографий, AddPhotoCommand — добавление новой фотографии в коллекцию.
Загрузка всех фотографий и логика получения новой фотографии будет в сервисе, реализующим интерфейс:
public interface IPhotogalleryVmService
{
Task> LoadItems();
Task GetPhotoFromUser();
}
VmService
будет для получения новой фотографии обращаться к сервису камеры устройства, реализация которого будет на каждой платформе своя, и, для загрузки фотографий из альбома, к сервису работы с альбомами.
public interface ICameraService
{
Task TakePhoto();
}
public interface IPhotoAlbumService
{
Task> 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();
vmLookupService.Register(typeof(PhotogalleryViewModel));
}
Проработка простой верстки на iOS и реализация платформенных сервисов
Сперва реализуем все платформенные сервисы. Начнем с сервиса камеры. Создадим в iOS проекте папку Services и добавим в нее CameraService:
public class CameraService : ICameraService
{
public Task TakePhoto()
{
throw new NotImplementedException();
}
}
public async Task 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> LoadPhotosFrom(string albumName)
{
throw new NotImplementedException();
}
}
public Task> LoadPhotosFrom(string albumName)
{
var photos = new List();
var tcs = new TaskCompletionSource>();
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>(() => photos);
}
}
else
return new Task>(() => 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();
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.
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();
manager.EnsurePlatformAdaptionLoaded();
MvxTrace.Trace("Auth plugin is loaded");
_loaded = true;
}
}
- Добавить в UI проект класс Plugin, в котором будет регистрироваться ViewController и платформенные сервисы.
public class Plugin : IMvxPlugin
{
public void Load()
{
var viewLookupService = Mvx.Resolve();
viewLookupService.Register();
Mvx.RegisterSingleton(() => new CameraService());
Mvx.RegisterSingleton(() => new PhotoAlbumService());
}
}
- Добавить в запускаемый проект класс
XMU_PhotogalleryPluginBootstrap
.
public class XMU_PhotogalleryPluginBootstrap
: MvxLoaderPluginBootstrapAction
{
}
- Прописать навигацию на фотогалерею из меню в конфиге.
{
"icon":"res:Images/Menu/photo.png",
"name":"Фотогалерея",
"type":"Photogallery.Core.ViewModels.Photogallery.IPhotogalleryViewModel"
}
- Добавить обработку события навигации в Core плагина.
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.
var routerService = Mvx.Resolve();
routerService.Register(new PhotogalleryRouterSubscriber());
Запустим наш проект и убедимся, что все работает как запланировали.
Заключение
Мы рассмотрели основные моменты при работе с нашей платформой. Главные мысли, которые хотелось донести:
- Вы ничем не ограничены;
- Попробуйте MvvmCross;
- Будьте новаторами.
Обсуждение появившихся в процессе чтения мыслей предлагаем перенести в комментарии. Спасибо, что прочитали!
Об авторах
Максим Евтух — Разработчик мобильных приложений на фреймворке Xamarin в компании «НОТИССИМУС». В мобильной разработке с 2013 года. В свободное время занимается изучением вопроса усовершенствования MvvmCross«а и поддержкой контрола GitHub для реализации новых гайдов Material Design.
Денис Кретов — технический директор в компании «НОТИССИМУС». Специализируется на разработке мобильных приложений для интернет-магазинов, а также решений на базе iBeacon.
Другие статьи из нашего блога о Xamarin читайте по ссылке #xamarincolumn.
Комментарии (1)
2 декабря 2016 в 13:55
0↑
↓
Все прочел, круто. Идея «xamarin cms» давно витает в воздухе. Сам начинал, даже билдер лайутов намастрячил летом, было дело на C# под windows. Но текущими проектами с тех пор завалило.А вот ссылок (хотя бы на страницу, даже не на репозитарий github) в статье не нашел. Невнимательно читал?