Особенности разработки под Xamarin.Forms +30


Около года назад появился кросс-платформенный фреймворк под названием Xamarin.Forms. Он позволяет создавать мобильные приложения под разные платформы, используя C# и .NET. По сути он является надстройкой над уже существовавшими до него Xamarin.iOS, Xamarin.Android и Xamarin.WinPhone. И, в отличие от них, он позволяет создавать лишь один проект, в котором можно описать всю логику работы приложения и его UI. А затем просто компилить его под разные платформы. В итоге, все это сильно экономит время.

Мы считаем, что эта платформа имеет свои перспективы и, потому не смогли пройти мимо нее. По традиции, мы начали с разработки Data Grid контрола. За время работы над ним у нас накопился интересный опыт разработки под Xamarin.Forms, и мы хотим с вами им поделиться.

Данная статья построена на базе моего доклада на DevCon 2015, и желающие могут ознакомиться с видео версией здесь.

Преимущества Xamarin.Forms


Для начала хочется рассказать о преимуществах этой платформы:
  • Во-первых, это всем нам хорошо знакомый C# и .NET. Если вы давно уже пишите на шарпах, то вам не надо тратить много времени на изучение нескольких новых фреймворков, а то и языков. Ну или, по крайней мере, в начале не надо, и вы можете достаточно быстро стартануть, используя свои текущие знания.
  • Во-вторых, подход к созданию и работе с пользовательским интерфейсом близок к тому, к чему мы все привыкли в Windows. Особенно рады будут разработчики WPF, так как Xamarin Forms поддерживает работу с XAML, биндинги, темплейты, стили и прочие радости жизни. Думаю, понятно, что они несколько урезаны и не стоит ожидать всей мощи WPF, но все-таки удобства это добавляет.
  • Так как это C#, то следующий плюс в том, что можно повторно использовать уже написанный код. В большей части он будет работать корректно. Есть у платформ ограничения, но они не столь велики. У нас получилось завести достаточно большой кусок из XtraGridControl-а, и это нам сильно помогло.
  • Из того, что Xamarin.Forms схож с WPF, вытекает следующий плюс этой платформы: MVVM. Действительно, Xamarin.Forms имеет XAML, визуальные элементы имеют BindingContext (аналог DataContext в WPF), есть BindableProperty (аналог DependencyProperty). Таким образом, можно связывать View с ViewModel аналогично тому, как в WPF.
  • Еще одно преимущество данной платформы в том, что так как UI описывается только в одном месте, то приложения под разными системами будут выглядеть очень похоже. Что может быть важно, например, в корпоративных разработках.


Особенности и недостатки Xamarin.Forms


Давайте теперь перейдем к нюансам и недостаткам.

Для начала небольшое техническое вступление. На следующей схеме я примерно показал, как работает Xamarin.Forms.



В верху схемы расположена PCL часть. По сути это и есть Xamarin.Forms. В общих чертах он представляет собой набор редакторов, навигационных панелей, лейаут панелей и так далее. При разработке UI большую часть времени работаешь как раз с ними. Однако, данные контролы — это всего лишь абстракция внутри PCL части. Чтобы они смогли как-то отобразиться на устройстве, существуют так называемые рендереры. Располагаются они на следующей ступени иерархии в Xamarin Platform частях.

Под PCL частью у нас расположены Xamarin.iOS, Xamarin.Android и Xamarin.WinPhone. Это по сути и есть тот Xamarin, который уже существовал до Xamarin.Forms. А что же такое этот Xamarin? Это C# обертки над нативными классами для каждой платформы. Так вот рендереры — это и есть такие обертки над соответствующими визуальными компонентами, но которые дополнительно внутри себя содержат ссылки на PCL объекты, умеют читать у них выставленные свойства и применять их у себя.

В дальнейшем эти рендереры уже разворачиваются в нативные контролы. Их мы можем видеть на нижнем уровне схемы.

Чтобы было меньше путаницы, приведу пример. Есть класс Button в PCL части. Каждая Xamarin Platform часть содержит класс ButtonRenderer, который хранит в себе экземпляр класса Button. В свою очередь ButtonRenderer’ы — это обертки над классами кнопок для каждой платформы, например UIButton в iOS. Именно по такой цепочке происходит работа с контролами из PCL части.

Вот в этом механизме и заключены почти все проблемы этой платформы.

Можно выделить следующие группы проблем, с которыми приходится сталкиваться при разработке на Xamarin.Forms:
  • Неполная реализация функционала WPF
  • Компромиссные решения в реализации функционала, различающегося на разных платформах
  • Различное поведение на разных платформах
  • Производительность


Теперь давайте рассмотрим эти проблемы на конкретных примерах.

Неполная реализация функционала WPF


Это первое, на что мы наткнулись. Xamarin.Forms имеет значительные ограничения в использовании темплейтов при разработке. Мне, как человеку, долго работавшему с WPF, этот инструмент очень нравится. Так как очень удобно, когда можно произвольно менять внешний вид контрола простым перекрытием темплейта. Однако, понятно, что в концепцию рендереров темплейты плохо вписываются, так как на конечных платформах подобное есть только в WinPhone.

Компромиссные решения в реализации функционала, различающегося на разных платформах


Платформы порой различаются, и рендерерам приходится все эти различия сводить к какому-то единому механизму управления. Таким образом, приходится жертвовать какой-то функциональностью.
  • Например, в Android у текстового редактора можно включить как однострочный режим, так и многострочный. Однако в iOS этого сделать нельзя. Там есть два разных контрола для этих ситуаций. Так как один PCL объект может быть свзан только с одним рендерером, а значит и с одним контролом на платформе, то в Xamarin.Forms оставили только одно поведение, и редактор в нем всегда многострочный.
  • Так же во вех мобильных платформах очень сильно различаются механизмы работы с жестами. И достаточно сложно их свести к какой-то общей абстракции. Поэтому Xamarin предоставляет возможность отловить только обычный тап (и то не без багов). Остальные жесты из PCL части отловить нельзя.
  • Иногда платформы идеологически не позволяют сделать что-то такое, что может С# или .NET. Данные ограничения описаны в документации у Xamarin. Однако, когда впервые с этим сталкиваешься, разобраться с ходу бывает непросто.

    Например, iOS требует, чтобы весь код был статический, в том смысле, что он не позволяет генерировать код в рантайме. Частично нам уже облегчили жизнь удалением пространства имен System.Reflection.Emit. Но иногда вполне можно написать код, который будет без проблем компилиться, запускаться и работать на эмуляторе, но упадет на устройстве. К счастью, как я уже писал, возможные проблемы описаны в документации.


Различное поведение на разных платформах


Иногда поведение контролов может различаться на разных платформах. Как вы уже видели, в Xamarin.Forms пользовательский интерфейс описывается в общей для всех платформ части. И вполне ожидается, что в итоге он будет выглядеть одинаково на всех системах. Ну или хотя бы очень близко. Но это бывает не всегда. Давайте пройдемся по основным граблям, на которые наступали мы.

Margins в WinPhone

В WinPhone у некоторых контролов большие маржины, чего нет на остальных платформах (например, у Switch контрола). Таким образом, внешний вид приложения на нем может очень сильно отличаться от версий на Android и iOS, вплоть до полной неработоспособности (когда элементы просто не поместятся в видимой области).

Покажу небольшой пример. Если создать тестовое приложение с подобным лейаутом:
<Grid RowSpacing="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Switch Grid.Row="0“ />
    <Switch Grid.Row=“1“ />
</Grid>


а потом запустить его под iOS, мы получим следующий внешний вид:



На Android будет тоже самое. Но если мы запустим это под WinPhone, то получим вот такой результат:



Как видим, Switch контролы расположены на удалении друг от друга. Причина этого в том, что внутри Switch контрола в WinPhone лежит Grid панель со слишком большим размером. Так как это поведение специфично под WinPhone, то и решать это надо не на уровне PCL, а в рендерере свича. Нативным решением под WinPhone было бы перекрытие дефолтного стиля для Switch контролов, либо перекрытие стилей только для требуемых свичей. Понятное дело, что в PCL части мы это сделать не сможем и надо уходить на уровень ниже, в Xamarin.WinPhone.

На уровне Xamarin.WinPhone мы можем задать новый дефолтный стиль, но не можем применить стиль только для нужных нам контролов. Так как нам надо было поменять внешний вид только тех свичей, которые лежат внутри грида, и не трогать остальные, то давайте решим эту задачу.

Как я писал выше, на уровне платформ контролы представлены их рендерерами. Так что, чтобы внести какие-либо изменения в контрол, надо править его рендерер. Так как сейчас наша задача поправить поведение только наших свичей и не затронуть остальные, то самое простое решение — это сделать наследника свича (и использовать его в дальнейшем) и написать рендерер на него (отнаследованный от стандартного ButtonRenderer). А в этом рендерере мы можем изменить визуальное дерево контрола, как захотим. Например, так:
Control switchControl = VisualTreeHelper.GetChild(Control, 0) as Control;
Border border = VisualTreeHelper.GetChild(switchControl, 0) as Border;
Grid grid = VisualTreeHelper.GetChild(border, 0) as Grid;
grid.Height = 40;


И нужно не забыть вернуть корректные размер в GetDesiredSize методе:
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) {
    SizeRequest result = base.GetDesiredSize(widthConstraint, heightConstraint);
    result.Request = new Size(result.Request.Width, 40);
    return result;
}


В итоге получим результат:



Перехват жестов

Для следующего примера предположим, у нас есть новая задача: нам надо заблокировать все жесты над какой-то областью экрана. Там лежит много контролов, и некоторые из них самостоятельно ловят жесты (например, редакторы и кнопки), так что единой точки входа для блокировки нет. В этом случае есть простое решение: положить прозрачную панель поверх требуемой области и выставить ей свойство InputTransparent в false. Это свойство как раз и отвечает за то, что жесты перестают прокидываться дальше по дереву элементов. Таким образом, если мы напишем что-то подобное:
<Grid>
	<Switch VerticalOptions="Center" HorizontalOptions="Center"/>
	<ContentView VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" InputTransparent="false"/>
</Grid>


то это должно сработать и Switch не должен нажиматься. И действительно в iOS все так и происходит. Но если проверить это в Android, то в нем Switch все-равно будет нажиматься. Как видим, Android игнорирует выставление свойства InputTransparent. Чтобы это исправить, послупим как и в предыдущем примере — создадим наследника ContentView и для него напишем следующий рендерер:
public class MyContentViewRenderer : Xamarin.Forms.Platform.Android.VisualElementRenderer<MyContentView> {
	public override bool DispatchTouchEvent(Android.Views.MotionEvent e) {
		return !Element.InputTransparent;
	}
}


Я перекрываю метод DispatchTouchEvent, который как раз отвечает за прокидывание жестов, и возвращаю на нем значение в зависимости от выставленного InputTransparent у нашего PCL объекта. Эту проблемы мы тоже решили.

Различие в лейауте по-умолчанию

У элементов могут различаться дефолтные значения свойств на разных платформах. Например, лейаут. Если мы попробуем запустить приложение с вот такой панелькой:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="DefaultLayout.MyContentPage">
    
    <Switch />
</ContentPage>


то получим вот такие результаты на iOS и WinPhone:



На Android будет аналогично тому, как на WinPhone. Как видим, контролы появились в разных местах экрана. Поэтому не следует всегда полагаться на дефолтные значения свойств. Лучше обезопасить себя и задать их значения, чтобы не получилось что-то подобное.

Производительность


Так же дополнительный уровень абстракций PCL части добавляет новых тормозов в приложение. По скорости приложения на Xamarin.Forms могут достаточно сильно проигрывать нативным. Особенно с учетом того, что он очень любит достаточно часто перерисовывать все визуальное дерево.

Как нам с этим жить?


Как я уже показывал, для обхождения подобных проблем приходится лезть в нативные мобильные части и писать свои рендереры на элементы, и в них уже решать то, с чем не справляются дефолтные рендереры.

Лично мы написали полностью свой обработчик жестов, позволяющий из PCL части подписываться на любые жесты, написали рендереры для всех эдиторов, вытаскивая туда недостающие свойства. Проблема с InputTransparent-ом опять же решалась рендерером в андроиде и блокировкой жестов в нем. Ну а когда лезешь в рендерер, уже приходится сталкиваться с нативным API той системы, в рендерере которой находишься. А это уже требует изучения этого API. Так что я не зря в начале упомянул, что это сначала не надо его учить, потом с этим все равно, скорее всего, столкнешься.

Но все эти проблемы так или иначе обходятся. И мы в итоге смогли написать наш GridControl на этом фреймворке. Он обладает достаточно большой функциональностью для отображения и работы с данными. Выглядит он вот так:



Grid получился достаточно шустрым и богатым по функциональности. Но подробнее о нем мы напишем в нашей следующей статье про Xamarin.Forms.

В качестве вывода хочется сказать, что Xamarin.Forms можно использовать, когда нужен быстрый старт сразу на нескольких платформах, когда не сильно важна производительность, или когда очень много старого кода, который хочется повторно использовать в новом приложении. Также он наверняка будет очень полезен корпоративным программистам, ибо там скорость разработки важнее красивостей конечного приложения.

Конечно же, если вам важен какой-то необычный дизайн, или высокая скорость работы, и вы пишете приложения для продажи в магазинах, то, может, лучше написать в нативе или в Xamarin Mobile. Хотя в данный момент команда Xamarin активно развивает свой продукт, достаточно часто выходят обновления. Так что можно надеяться, что область применения Xamarin.Forms будет расширяться.

Update:
Получить более подробную информацию о GridControl-е можно в нашей статье: Бесплатный грид контрол для Xamarin от DevExpress




К сожалению, не доступен сервер mySQL