В статье речь пойдет о Xamarin.Forms на примере живого проекта. Кратко поговорим о том, что такое Xamarin.Forms, сравним с похожей технологией WPF, увидим, как достигается кроссплатформенность. Также разберём узкие места, с которыми мы столкнулись в процессе разработки, и добавим немного реактивного программирования с ReactiveUI.
В современном мире, в век огромного количества различных устройств и операционных систем, более конкурентноспособными являются приложения, способные работать сразу на нескольких платформах. Для реализации кроссплатформенности существуют разные подходы: написание нативного кода для каждой целевой платформы (по сути, нескольких различных приложений) или использование специальных фреймворков, позволяющих разрабатывать единый код для всех случаев. Есть еще кроссплатформенные языки программирования или же среды исполнения, например, Java Virtual Machine, но здесь я их касаться не буду.
Нативный подход обеспечивает более высокое качество и скорость работы готовых приложений, позволяет более эффективно использовать ресурсы платформы. Однако, сроки разработки значительно больше, чем при написании единого кода. Увеличивается также и стоимость поддержки таких продуктов. Использование сторонних фреймворков для написания единой кодовой базы, напротив, позволяет писать приложения быстрее, используя более распространённые языки программирования, но может добавить накладных расходов при работе уже готового программного обеспечения, ведь любой инструмент, обеспечивающий кроссплатформенность, является своего рода обёрткой, транслирующей вызовы к системе и от неё. Никто не отменял и банальных погрешностей в работе самой этой обёртки.
Какое решение использовать на конкретном проекте – выбор сугубо индивидуальный и зависит от многих параметров, таких как сроки, квалификация разработчиков, требования к приложению и т.д.
Не так давно нашей команде пришлось лицом к лицу столкнуться с кроссплатформенной разработкой. Задача от заказчика звучала так:
Сроки и бюджет говорили о том, что создание нескольких приложений – в данном случае неподходящее решение. Выгоднее будет разработать единую кодовую базу для двух платформ с возможностью добавления индивидуального функционала там, где это необходимо. В нашем случае стек технологий был продиктован заказчиком: языком программирования был выбран C#, а инструментом для кроссплатформенной разработки – Xamarin.Forms.
Xamarin – это инструмент для создания приложений на языках семейства .NET (C#, F#, Visual Basic), который позволяет создавать единый код, работающий на Android, iOS и Windows (UWP-приложения). Это xaml-подобная технология, то есть интерфейс описывается декларативно в формате xml, вы сразу видите, как элементы расположены на форме и какие свойства имеют. Такой подход очень удобен, в отличие, например, от Windows.Forms, в котором, если бы не графический редактор, разрабатывать и редактировать пользовательские интерфейсы было бы крайне сложно, так как все элементы и их свойства создаются динамически. У меня был опыт разработки подобных интерфейсов без декларативных описаний в среде, не имеющей удобного графического редактора, и я не хочу его повторять. В Xamarin.Forms сохранена возможность динамического создания элементов интерфейса в программном коде, но для чистоты кармы и благодарности от последователей вашего кода всё, что можно описать декларативно, лучше так и описывать.
Еще одной известной xaml-подобной технологией является WPF (Windows Presentation Foundation). Это потрясающий инструмент для создания красивых интерактивных пользовательских интерфейсов. На мой взгляд, это один из лучших инструментов, созданных Microsoft, имеющий, правда, серьезный недостаток – такие приложения можно разрабатывать только под Windows.
Xamarin.Forms включает в себя урезанную версию WPF, агрегируя лишь те компоненты, которые имеют аналоги на других платформах. С одной стороны, это даёт разработчикам, имеющим опыт работы с WPF, неплохую фору для входа в Xamarin.Forms. С другой же, познав всю прелесть и возможности WPF, программисту будет трудно то тут, то там натыкаться на отсутствие всех этих преимуществ в Xamarin.Forms. Мне было крайне грустно осознать, что я не могу использовать шаблоны для элементов управления, которыми так привыкла жонглировать в WPF, в Xamarin.Forms их попросту нет. Или удобный и мощный ItemControl – компонент WPF, позволяющий разместить на форме список элементов, задавая каким угодно образом их внешний вид и расположение. С помощью одного только этого компонента как основного можно написать, например, простенький прототип игры в бильярд. В Xamarin.Forms существуют аналоги ItemControl, но все они имеют конкретную реализацию внешнего вида списка, что лишает их необходимой гибкости и вынуждает разработчиков идти на GitHub за «велосипедами».
Однако не бывает худа без добра. Своими ограничениями Xamarin.Forms платит за кроссплатформенность, которая действительно легко и удобно достигается средствами этого инструмента. Вы пишете общий код для iOS, Android и Windows, а Xamarin сам разбирается, как связать ваш код с родным для каждой платформы API. Кроме того, есть возможность писать не только общий, но и платформозависимый код, и Xamarin тоже поймет, что и где вызывать. Одним из главных механизмов достижения кроссплатформенности является наличие «умных» сервисов, способных осуществлять кросс-зависимые вызовы, то есть обращаться к той или иной реализации определённого функционала в зависимости от платформы. Платформозависимый код можно писать не только на C#, но и добавлять его в xaml-разметку. В нашем случае iOS версия была урезанной и часть графического интерфейса нужно было скрыть, размеры некоторых элементов также зависели от платформы.
Для большей наглядности приведу небольшой пример. В нашем проекте мы использовали библиотеку классов, предоставленную заказчиком, которая была написана на C++. Назовём её MedicalLib. MedicalLib собиралась в две разные сборки в зависимости от платформы (статическая MedicalLib.a для iOS и динамическая MedicalLib.dll для Windows). Кроме того, она имела различные wrapper-классы (классы-переходники) для вызова неуправляемого кода. Основной проект (общая кодовая база) содержал описание API MedicalLib (интерфейс), а платформозависимые проекты для iOS и Windows – конкретные реализации этого интерфейса и ссылки на сборки MedicalLib.a и MedicalLib.dll соответственно. Из основного кода мы вызывали те или иные функции абстракции, не задумываясь, как именно они реализованы, а механизмы Xamarin.Forms, понимая, на какой платформе запущено приложение, вызывали необходимую реализацию и подгружали конкретную сборку.
Примерно так это можно изобразить схематично:
Для написания программ с Xamarin.Forms используется Visual Studio. На MacOS – Visual Studio for Mac. Многие разработчики считают это больше недостатком, ссылаясь на тяжеловесность этой IDE. Для меня Visual Studio является привычной средой разработки и на высокопроизводительных компьютерах не доставляет каких-либо неудобств. Хотя Mac-версия этой IDE пока еще далека от идеала и имеет на порядок больше недочетов, чем её Windows-собрат. Для мобильной разработки имеется целый ряд встроенных эмуляторов мобильных устройств, а также возможность подключить реальный девайс для отладки.
В основу любого проекта Xamarin.Forms отлично ложится паттерн проектирования MVVM (Model – View – View Model), главным принципом которого является отделение внешнего вида пользовательского интерфейса от бизнес-логики. В нашем случае мы использовали MVVM, что действительно оказалось удобно. Важным моментом реализации MVVM является механизм оповещений View о том, что какие-то данные изменились и это необходимо отобразить на интерфейсе. Думаю, многие разработчики на WPF слышали об интерфейсе INotifyPropertyChanged, реализуя который во вью-моделях, мы получаем возможность оповещать интерфейс об изменениях. Этот способ имеет свои плюсы и минусы, но главным недостатком является запутанность и громоздкость кода в случаях, когда во вью-моделях есть вычисляемые свойства (например, Name, Surname и вычисляемое FullName). Мы выбрали более удобный фреймворк ReactiveUI. Он уже содержит реализацию INotifyPropertyChanged, а также много других преимуществ – например, IObservable.
IObservable – это реактивные push-based провайдеры уведомлений о наличии обновлений для подписчиков. Очень похоже на события и подписки на них, но с рядом дополнительных встроенных фич. Например, мы можем реагировать не на все обновления, а с какими-нибудь фильтрами (допустим, наш IObservable – «поток» целых чисел, и мы хотим принимать во внимание только четные). Или одним подписчиком можно подписаться на комбинацию из двух IObservable, первый из которых типа bool, и реагировать или не реагировать на обновления второго в зависимости от того, что пришло в первый. IObservable можно представить как поток данных, хотя по сути это коллекция.
Если коротко и не вдаваясь в подробности описывать предметную область нашего приложения, то это симулятор реального медицинского оборудования. На экране бегут графики якобы снимаемых с пациента данных, а пользователь, то есть врач, может менять настройки девайса и смотреть, как это влияет на медицинские характеристики пациента. Интерфейс в нашем случае – это отображение в реальном времени данных, которые с большой частотой (раз в 40 миллисекунд) приходят из недр приложения.
Реализовано это было следующим образом: на бэкэнде постоянно крутился некий генератор данных, которые поступали в несколько IObservable. На каждый IObservable во вью-моделях были подписаны соответствующие свойства, которые с помощью механизмов ReactiveUI выводили данные на интерфейс в нужном виде.
Взаимодействие с интерфейсом в обратном направлении (т.е. обработка действий пользователя), было реализовано с помощью так называемых Interaction – взаимодействий, которые инициировались при работе пользователя с UI (например, при нажатии на кнопку), а обрабатывались в любом месте приложения. Что-то наподобие интерфейса ICommand и команд в WPF, только интеракции в нашем случае назначались на интерфейсные элементы не декларативно (как с командами в WPF), а программно, что показалось не очень удобным.
Выше я уже проводила аналогию с WPF. Для наглядности покажу, как выглядит наша архитектура в сравнении со стандартным WPF-приложением:
Весь набор инструментов, описанных на этой схеме, мы получили, используя Reactive UI.
А теперь я перейду от красивой теории к практическим проблемам, с которыми пришлось столкнуться, ведь ни один живой проект без этого не обходится.
Наиболее сложные моменты были связаны с разработкой UWP приложения, Windows добавила немало головной боли из-за некоторых особенностей и ограничений в отношении этих программ.
При сворачивании окна приложение переходит в состояние Suspended, и Windows выделяет ему меньше ресурсов. Это было критично, так как в одной из вариаций наш Data Generator работал, интегрируясь с внешними источниками и получая данные через сокеты. При сворачивании окна сокеты продолжали получать данные, заполняя свой внутренний буфер пакетами. А при выходе из режима Suspended все эти пакеты тут же помещались в буфер приложения, начиная обрабатываться только в этот момент. Из-за этого происходила задержка в отображении данных после развертывания окна, которая была пропорциональна времени, проведенному в свёрнутом виде. Всё решилось грамотной обработкой событий Suspending и Resuming, при наступлении которых мы сохраняли состояние приложения и закрывали сокеты, открывая их снова при восстановлении штатного режима работы.
Ещё одной непростой задачей стало добавление второго окна приложения (это было необходимо в рамках технического задания). Сейчас я не могу сказать, были ли это ограничения UWP, Xamarin.Forms или же наша собственная ошибка – краш происходил в конструкторе одного из представлений из-за UI-потоков. Позже выяснилось, что библиотека, предоставленная заказчиком и являющаяся ядром обработки данных, – однопользовательская и хранит в себе состояние. Таким образом, два независимых окна требовали двух независимых экземпляров библиотеки, что добавило нам одну большую низкоуровневую задачу, решение которой не было бы тривиальным. Мы решили, что овчинка выделки не стоит и проще не открывать отдельное окно, а создавать новый экземпляр приложения со своими настройками и адресным пространством. В итоге со стороны пользователя это выглядело в точности как второе окно (даже при закрытии главного окна второе тоже завершало свою деятельность), и разницу можно было заметить, только заглянув в диспетчер задач и увидев второй процесс.
Ну, и главный недостаток UWP, с которым пришло немало повозиться, – это политика его распространения. Чтобы установить приложение, его либо необходимо загрузить в Microsoft Store и скачивать оттуда, либо поставлять дистрибутив другим способом с одним ограничением – тогда при его установке необходимо дать операционной системе соответствующие разрешения («Sideloaded Apps» в настройках Windows). Техническое задание требовало реализации второго подхода, однако политика безопасности заказчика запрещала сотрудникам компании менять подобные настройки. В итоге нам пришлось написать инсталлер, который перед установкой включал «Sideloaded Apps», устанавливал пакет и в конце выключал эту настройку (естественно, по согласованию с заказчиком).
Отдельно стоит отметить, что сама кроссплатформенная разработка обязует программиста быть более дисциплинированным и проверять свой код на всех целевых платформах, что, конечно, добавляет монотонности.
Что касается архитектуры и каких ошибок можно было бы избежать?
Выше я уже писала об использовании для взаимодействия с пользователем Interaction от ReactiveUI. Такие интеракции можно обработать в любом месте приложения, что является не только их достоинством, но и недостатком, так как может внести большую путаницу. Чтобы понять, почему при нажатии на кнопку она окрашивается в красный цвет, а не в зелёный, вам нужно найти все обработчики интеракции и проверить код в них. Договоренность внутри команды обрабатывать интеракции унифицированно может помочь снизить негативное влияние этого фактора, но избежать его совсем вряд ли удастся.
Не меньшим по значимости недостатком описанных выше интеракций является то, что управлять порядком вызовов обработчиков практически невозможно. Мы наступили еще на одни грабли и использовали их не только для взаимодействия с пользователем, но и для общения вью-моделей друг с другом. Это часто играло против нас, так как из-за неконтролируемости вызовов обработчиков мы получали много плавающих багов. Поэтому использовать Interaction я рекомендую с большой осторожностью, а при реализации MVVM прорабатывать иные подходы к взаимодействию вью-моделей.
Еще стоит отметить, что достаточно много времени наша команда потратила на адаптацию визуальной части пользовательского интерфейса под Windows-приложение. Сжатые сроки и приоритеты заказчика поставили разработку iOS-версии на первое место, поэтому к UWP мы приступили, имея уже наполовину готовый проект, работающий на iOS. Изначально не предполагалось изменение разрешения, и приложение оказалось не готовым к свободному масштабированию окна, не имея достаточной гибкости в расположении элементов, что привело нас к довольно объемному рефакторингу всех визуальных компонентов. Поэтому не могу не отметить, что разработка кроссплатформенного проекта всё-таки должна вестись параллельно на всех предполагаемых платформах.
Xamarin.Forms часто считают сырой и недоработанной технологией. Не могу сказать, что наш проект столкнулся с какими-то критичными проблемами именно со стороны Xamarin. Опыт нашей команды подтверждает, что адаптация приложения под вторую платформу – процесс достаточно простой и быстрый. Не всё прошло гладко и без проблем, и задачи по адаптации всё-таки были – например, из-за различия работы файловых систем или визуального отображения некоторых стилей. Но все эти проблемы либо устранялись достаточно быстро, либо не имели весомого значения для заказчика.
Благодаря архитектуре и выбранным решениям, со всеми их достоинствами и недостатками, проект уже полтора года наращивает функционал без какого-либо глобального рефакторинга и чувствует себя твёрдо стоящим на ногах. Инструмент работает и со своими основными задачами справляется. Кроме того, он не является новым для IT-сообщества, поэтому документации, которая помогает разобраться в тонкостях, достаточно. То, что теперь кроссплатформенную разработку можно вести на платформе .NET и удобном С#, которые предпочитают многие программисты, является неоспоримым преимуществом и может стать финальным аккордом в решении использовать Xamarin.Forms и на вашем проекте.
Кроссплатформа — что выбрать?
В современном мире, в век огромного количества различных устройств и операционных систем, более конкурентноспособными являются приложения, способные работать сразу на нескольких платформах. Для реализации кроссплатформенности существуют разные подходы: написание нативного кода для каждой целевой платформы (по сути, нескольких различных приложений) или использование специальных фреймворков, позволяющих разрабатывать единый код для всех случаев. Есть еще кроссплатформенные языки программирования или же среды исполнения, например, Java Virtual Machine, но здесь я их касаться не буду.
Нативный подход обеспечивает более высокое качество и скорость работы готовых приложений, позволяет более эффективно использовать ресурсы платформы. Однако, сроки разработки значительно больше, чем при написании единого кода. Увеличивается также и стоимость поддержки таких продуктов. Использование сторонних фреймворков для написания единой кодовой базы, напротив, позволяет писать приложения быстрее, используя более распространённые языки программирования, но может добавить накладных расходов при работе уже готового программного обеспечения, ведь любой инструмент, обеспечивающий кроссплатформенность, является своего рода обёрткой, транслирующей вызовы к системе и от неё. Никто не отменял и банальных погрешностей в работе самой этой обёртки.
Какое решение использовать на конкретном проекте – выбор сугубо индивидуальный и зависит от многих параметров, таких как сроки, квалификация разработчиков, требования к приложению и т.д.
А теперь к сути
Не так давно нашей команде пришлось лицом к лицу столкнуться с кроссплатформенной разработкой. Задача от заказчика звучала так:
- Создать iOS-приложение, работающее на iPad;
- Разработать такое же приложение с расширенным функционалом под Windows 10;
- Всё это при условии, что правки в дальнейшем могут вноситься как в оба приложения одновременно, так и по отдельности;
- Сделать приложение максимально гибким, поддерживаемым и расширяемым, потому что техническое задание, как обычно, менялось со скоростью света.
Сроки и бюджет говорили о том, что создание нескольких приложений – в данном случае неподходящее решение. Выгоднее будет разработать единую кодовую базу для двух платформ с возможностью добавления индивидуального функционала там, где это необходимо. В нашем случае стек технологий был продиктован заказчиком: языком программирования был выбран C#, а инструментом для кроссплатформенной разработки – Xamarin.Forms.
Xamarin – это инструмент для создания приложений на языках семейства .NET (C#, F#, Visual Basic), который позволяет создавать единый код, работающий на Android, iOS и Windows (UWP-приложения). Это xaml-подобная технология, то есть интерфейс описывается декларативно в формате xml, вы сразу видите, как элементы расположены на форме и какие свойства имеют. Такой подход очень удобен, в отличие, например, от Windows.Forms, в котором, если бы не графический редактор, разрабатывать и редактировать пользовательские интерфейсы было бы крайне сложно, так как все элементы и их свойства создаются динамически. У меня был опыт разработки подобных интерфейсов без декларативных описаний в среде, не имеющей удобного графического редактора, и я не хочу его повторять. В Xamarin.Forms сохранена возможность динамического создания элементов интерфейса в программном коде, но для чистоты кармы и благодарности от последователей вашего кода всё, что можно описать декларативно, лучше так и описывать.
Xamarin.Forms и WPF
Еще одной известной xaml-подобной технологией является WPF (Windows Presentation Foundation). Это потрясающий инструмент для создания красивых интерактивных пользовательских интерфейсов. На мой взгляд, это один из лучших инструментов, созданных Microsoft, имеющий, правда, серьезный недостаток – такие приложения можно разрабатывать только под Windows.
Xamarin.Forms включает в себя урезанную версию WPF, агрегируя лишь те компоненты, которые имеют аналоги на других платформах. С одной стороны, это даёт разработчикам, имеющим опыт работы с WPF, неплохую фору для входа в Xamarin.Forms. С другой же, познав всю прелесть и возможности WPF, программисту будет трудно то тут, то там натыкаться на отсутствие всех этих преимуществ в Xamarin.Forms. Мне было крайне грустно осознать, что я не могу использовать шаблоны для элементов управления, которыми так привыкла жонглировать в WPF, в Xamarin.Forms их попросту нет. Или удобный и мощный ItemControl – компонент WPF, позволяющий разместить на форме список элементов, задавая каким угодно образом их внешний вид и расположение. С помощью одного только этого компонента как основного можно написать, например, простенький прототип игры в бильярд. В Xamarin.Forms существуют аналоги ItemControl, но все они имеют конкретную реализацию внешнего вида списка, что лишает их необходимой гибкости и вынуждает разработчиков идти на GitHub за «велосипедами».
Кроссплатформенность в Xamarin.Forms
Однако не бывает худа без добра. Своими ограничениями Xamarin.Forms платит за кроссплатформенность, которая действительно легко и удобно достигается средствами этого инструмента. Вы пишете общий код для iOS, Android и Windows, а Xamarin сам разбирается, как связать ваш код с родным для каждой платформы API. Кроме того, есть возможность писать не только общий, но и платформозависимый код, и Xamarin тоже поймет, что и где вызывать. Одним из главных механизмов достижения кроссплатформенности является наличие «умных» сервисов, способных осуществлять кросс-зависимые вызовы, то есть обращаться к той или иной реализации определённого функционала в зависимости от платформы. Платформозависимый код можно писать не только на C#, но и добавлять его в xaml-разметку. В нашем случае iOS версия была урезанной и часть графического интерфейса нужно было скрыть, размеры некоторых элементов также зависели от платформы.
Для большей наглядности приведу небольшой пример. В нашем проекте мы использовали библиотеку классов, предоставленную заказчиком, которая была написана на C++. Назовём её MedicalLib. MedicalLib собиралась в две разные сборки в зависимости от платформы (статическая MedicalLib.a для iOS и динамическая MedicalLib.dll для Windows). Кроме того, она имела различные wrapper-классы (классы-переходники) для вызова неуправляемого кода. Основной проект (общая кодовая база) содержал описание API MedicalLib (интерфейс), а платформозависимые проекты для iOS и Windows – конкретные реализации этого интерфейса и ссылки на сборки MedicalLib.a и MedicalLib.dll соответственно. Из основного кода мы вызывали те или иные функции абстракции, не задумываясь, как именно они реализованы, а механизмы Xamarin.Forms, понимая, на какой платформе запущено приложение, вызывали необходимую реализацию и подгружали конкретную сборку.
Примерно так это можно изобразить схематично:
Среда разработки
Для написания программ с Xamarin.Forms используется Visual Studio. На MacOS – Visual Studio for Mac. Многие разработчики считают это больше недостатком, ссылаясь на тяжеловесность этой IDE. Для меня Visual Studio является привычной средой разработки и на высокопроизводительных компьютерах не доставляет каких-либо неудобств. Хотя Mac-версия этой IDE пока еще далека от идеала и имеет на порядок больше недочетов, чем её Windows-собрат. Для мобильной разработки имеется целый ряд встроенных эмуляторов мобильных устройств, а также возможность подключить реальный девайс для отладки.
Reactive UI и реактивная модель
В основу любого проекта Xamarin.Forms отлично ложится паттерн проектирования MVVM (Model – View – View Model), главным принципом которого является отделение внешнего вида пользовательского интерфейса от бизнес-логики. В нашем случае мы использовали MVVM, что действительно оказалось удобно. Важным моментом реализации MVVM является механизм оповещений View о том, что какие-то данные изменились и это необходимо отобразить на интерфейсе. Думаю, многие разработчики на WPF слышали об интерфейсе INotifyPropertyChanged, реализуя который во вью-моделях, мы получаем возможность оповещать интерфейс об изменениях. Этот способ имеет свои плюсы и минусы, но главным недостатком является запутанность и громоздкость кода в случаях, когда во вью-моделях есть вычисляемые свойства (например, Name, Surname и вычисляемое FullName). Мы выбрали более удобный фреймворк ReactiveUI. Он уже содержит реализацию INotifyPropertyChanged, а также много других преимуществ – например, IObservable.
IObservable – это реактивные push-based провайдеры уведомлений о наличии обновлений для подписчиков. Очень похоже на события и подписки на них, но с рядом дополнительных встроенных фич. Например, мы можем реагировать не на все обновления, а с какими-нибудь фильтрами (допустим, наш IObservable – «поток» целых чисел, и мы хотим принимать во внимание только четные). Или одним подписчиком можно подписаться на комбинацию из двух IObservable, первый из которых типа bool, и реагировать или не реагировать на обновления второго в зависимости от того, что пришло в первый. IObservable можно представить как поток данных, хотя по сути это коллекция.
Если коротко и не вдаваясь в подробности описывать предметную область нашего приложения, то это симулятор реального медицинского оборудования. На экране бегут графики якобы снимаемых с пациента данных, а пользователь, то есть врач, может менять настройки девайса и смотреть, как это влияет на медицинские характеристики пациента. Интерфейс в нашем случае – это отображение в реальном времени данных, которые с большой частотой (раз в 40 миллисекунд) приходят из недр приложения.
Реализовано это было следующим образом: на бэкэнде постоянно крутился некий генератор данных, которые поступали в несколько IObservable. На каждый IObservable во вью-моделях были подписаны соответствующие свойства, которые с помощью механизмов ReactiveUI выводили данные на интерфейс в нужном виде.
Взаимодействие с интерфейсом в обратном направлении (т.е. обработка действий пользователя), было реализовано с помощью так называемых Interaction – взаимодействий, которые инициировались при работе пользователя с UI (например, при нажатии на кнопку), а обрабатывались в любом месте приложения. Что-то наподобие интерфейса ICommand и команд в WPF, только интеракции в нашем случае назначались на интерфейсные элементы не декларативно (как с командами в WPF), а программно, что показалось не очень удобным.
Выше я уже проводила аналогию с WPF. Для наглядности покажу, как выглядит наша архитектура в сравнении со стандартным WPF-приложением:
Весь набор инструментов, описанных на этой схеме, мы получили, используя Reactive UI.
Сложности в процессе разработки
А теперь я перейду от красивой теории к практическим проблемам, с которыми пришлось столкнуться, ведь ни один живой проект без этого не обходится.
Наиболее сложные моменты были связаны с разработкой UWP приложения, Windows добавила немало головной боли из-за некоторых особенностей и ограничений в отношении этих программ.
При сворачивании окна приложение переходит в состояние Suspended, и Windows выделяет ему меньше ресурсов. Это было критично, так как в одной из вариаций наш Data Generator работал, интегрируясь с внешними источниками и получая данные через сокеты. При сворачивании окна сокеты продолжали получать данные, заполняя свой внутренний буфер пакетами. А при выходе из режима Suspended все эти пакеты тут же помещались в буфер приложения, начиная обрабатываться только в этот момент. Из-за этого происходила задержка в отображении данных после развертывания окна, которая была пропорциональна времени, проведенному в свёрнутом виде. Всё решилось грамотной обработкой событий Suspending и Resuming, при наступлении которых мы сохраняли состояние приложения и закрывали сокеты, открывая их снова при восстановлении штатного режима работы.
Ещё одной непростой задачей стало добавление второго окна приложения (это было необходимо в рамках технического задания). Сейчас я не могу сказать, были ли это ограничения UWP, Xamarin.Forms или же наша собственная ошибка – краш происходил в конструкторе одного из представлений из-за UI-потоков. Позже выяснилось, что библиотека, предоставленная заказчиком и являющаяся ядром обработки данных, – однопользовательская и хранит в себе состояние. Таким образом, два независимых окна требовали двух независимых экземпляров библиотеки, что добавило нам одну большую низкоуровневую задачу, решение которой не было бы тривиальным. Мы решили, что овчинка выделки не стоит и проще не открывать отдельное окно, а создавать новый экземпляр приложения со своими настройками и адресным пространством. В итоге со стороны пользователя это выглядело в точности как второе окно (даже при закрытии главного окна второе тоже завершало свою деятельность), и разницу можно было заметить, только заглянув в диспетчер задач и увидев второй процесс.
Ну, и главный недостаток UWP, с которым пришло немало повозиться, – это политика его распространения. Чтобы установить приложение, его либо необходимо загрузить в Microsoft Store и скачивать оттуда, либо поставлять дистрибутив другим способом с одним ограничением – тогда при его установке необходимо дать операционной системе соответствующие разрешения («Sideloaded Apps» в настройках Windows). Техническое задание требовало реализации второго подхода, однако политика безопасности заказчика запрещала сотрудникам компании менять подобные настройки. В итоге нам пришлось написать инсталлер, который перед установкой включал «Sideloaded Apps», устанавливал пакет и в конце выключал эту настройку (естественно, по согласованию с заказчиком).
Отдельно стоит отметить, что сама кроссплатформенная разработка обязует программиста быть более дисциплинированным и проверять свой код на всех целевых платформах, что, конечно, добавляет монотонности.
Что касается архитектуры и каких ошибок можно было бы избежать?
Выше я уже писала об использовании для взаимодействия с пользователем Interaction от ReactiveUI. Такие интеракции можно обработать в любом месте приложения, что является не только их достоинством, но и недостатком, так как может внести большую путаницу. Чтобы понять, почему при нажатии на кнопку она окрашивается в красный цвет, а не в зелёный, вам нужно найти все обработчики интеракции и проверить код в них. Договоренность внутри команды обрабатывать интеракции унифицированно может помочь снизить негативное влияние этого фактора, но избежать его совсем вряд ли удастся.
Не меньшим по значимости недостатком описанных выше интеракций является то, что управлять порядком вызовов обработчиков практически невозможно. Мы наступили еще на одни грабли и использовали их не только для взаимодействия с пользователем, но и для общения вью-моделей друг с другом. Это часто играло против нас, так как из-за неконтролируемости вызовов обработчиков мы получали много плавающих багов. Поэтому использовать Interaction я рекомендую с большой осторожностью, а при реализации MVVM прорабатывать иные подходы к взаимодействию вью-моделей.
Еще стоит отметить, что достаточно много времени наша команда потратила на адаптацию визуальной части пользовательского интерфейса под Windows-приложение. Сжатые сроки и приоритеты заказчика поставили разработку iOS-версии на первое место, поэтому к UWP мы приступили, имея уже наполовину готовый проект, работающий на iOS. Изначально не предполагалось изменение разрешения, и приложение оказалось не готовым к свободному масштабированию окна, не имея достаточной гибкости в расположении элементов, что привело нас к довольно объемному рефакторингу всех визуальных компонентов. Поэтому не могу не отметить, что разработка кроссплатформенного проекта всё-таки должна вестись параллельно на всех предполагаемых платформах.
Итог
Xamarin.Forms часто считают сырой и недоработанной технологией. Не могу сказать, что наш проект столкнулся с какими-то критичными проблемами именно со стороны Xamarin. Опыт нашей команды подтверждает, что адаптация приложения под вторую платформу – процесс достаточно простой и быстрый. Не всё прошло гладко и без проблем, и задачи по адаптации всё-таки были – например, из-за различия работы файловых систем или визуального отображения некоторых стилей. Но все эти проблемы либо устранялись достаточно быстро, либо не имели весомого значения для заказчика.
Благодаря архитектуре и выбранным решениям, со всеми их достоинствами и недостатками, проект уже полтора года наращивает функционал без какого-либо глобального рефакторинга и чувствует себя твёрдо стоящим на ногах. Инструмент работает и со своими основными задачами справляется. Кроме того, он не является новым для IT-сообщества, поэтому документации, которая помогает разобраться в тонкостях, достаточно. То, что теперь кроссплатформенную разработку можно вести на платформе .NET и удобном С#, которые предпочитают многие программисты, является неоспоримым преимуществом и может стать финальным аккордом в решении использовать Xamarin.Forms и на вашем проекте.
Vindicar
Любопытно.
Сам я чисто десктопный code-monkey, но порывался освоить андроид пару раз, в т.ч. Xamarin. Оба раза бросал это дело, потерпев неудачу с hello world (в окне с полем ввода и кнопкой элементы наезжали друг на друга и растащить их не удавалось).
В третий раз попробовать, что ли…
Almet
Начните с WPF и MVVM, будет намного проще.