Xamarin и Xamarin.Forms – кактус в шоколаде. Часть 2

от автора

Совсем недавно мы опубликовали статью про особенности и проблемы популярного мобильного фреймворка Xamarin. Сегодня же мы продолжим рассказ и сосредоточимся на нюансах библиотеки Xamarin.Forms. Под катом вас ждёт история о том, какие грабли поджидают решившего сделать кроссплатформенный UI.

Базовые проблемы

Для начала, вёрстку можно готовить как в коде, так и в формате XAML. К сожалению, превью интерфейса в реальном времени вы посмотреть не сможете, хотя для нативных средств разработки такая возможность доступна. Поэтому мы выбрали разработку интерфейса из кода. Выглядеть это будет немного громоздко, но в целом — удобно:

public class LoginViewController: ContentPage {     public LoginViewController()     {         Content = new StackLayout                     {                         Orientation = StackOrientation.Vertical,                         Children =                         {                             new Entry                             {                                 Placeholder = "Эл. почта",                                 Keyboard = Keyboard.Email,                             },                             new Entry                             {                                 Placeholder = "Пароль",                                 IsPassword = true,                             },                             new Button                             {                                 Text = "Войти"                             },                             new ActivityIndicator                             {                                 IsRunning = true,                                 IsVisible = false,                             }                         }                     };     } } 

Далее, набор компонентов в Xamarin.Forms не очень большой. Не хватает таких, казалось бы, банальных вещей, как например «карусели» для кастомного содержимого. Есть полноэкранный контроллер-карусель, но нам нужен был подобный компонент, занимающий только часть экрана. Пришлось немного погнуть один из сторонних велосипедов.

У тех компонентов, что имеются в наличии, часто не хватает свойств или событий, имеющихся на iOS или Android. Может отсутствовать возможность поменять шрифт placeholder’а или цвет курсора, установить максимальную длину у текстового поля и так далее, подобные вещи приходится дописывать самостоятельно. В вышедшей в середине ноября 2015 года версии Xamarin.Forms 2.0 часть таких свойств добавлена, но до 100% покрытия всех возможностей нативных платформ ещё далеко.

Не радует и невозможность выставлять у всех компонентов отступы (padding и margin) — они есть только у контейнеров. Хотите у кнопки или поля ввода сделать отступ? Оберните её в контейнер:

new ContentView     {         Padding = new Thickness                     {                         Top = Sizes.StandartTopPadding                         Left = Sizes.StandartLeftPadding                     },         Content = new Label                     {                         Text ="Текст с отступами"                     }     } 

Но слишком глубокая иерархия замедляет процесс рендеринга, который у Forms в принципе несколько более медленней, чем у нативных компонентов. Особенно замедление заметно на списках, но и просто достаточно сложные формы для некоторых приложений могут стать серьёзной проблемой.

Не меньше радует, что часть возможностей реализована некорректно и это является “фичей”. Например, при использовании стандартной навигации в Android у контроллеров при переходе на новый экран не будет вызываться часть событий жизненного цикла, т.к. навигация происходит не по реальным экранам или фрагментам, а банальной сменой вьюшки в рамках одного физического экрана.

Баги

Так же у компонентов часто встречаются баги. Например, у ScrollView была проблема — при появлении клавиатуры можно было прокрутить скролл дальше, чем нужно, в область без контента.

Источник проблемы — содержимое ScrollView по высоте меньше, чем контейнер. Размеры области для прокрутки содержимого определяет вот такой код:

protected override void LayoutChildren(double x, double y, double width, double height) {     //[...]     ContentSize=new Size(width, Math.Max(height, Content.Bounds.Bottom + Padding.Bottom)); } 

В результате появилась идея как быстро (и грязно) можно порешать проблему — создать наследника ScrollView с перекрытием нужного метода:

protected override void LayoutChildren(double x, double y, double width, double height) {     //[...]     //выкинули Max, размер контента всегда определяется размером контента     ContentSize = new Size(width, Content.Bounds.Bottom + Padding.Bottom); } 

Просто? Как бы не так — свойство ContentSize имеет приватный сеттер и в наследнике его значение просто так не изменить. Но раз уж мы пошли по кривой дорожке — всегда можно позвать на помощь рефлексию и таки изменить значение свойства.

public class ScrollViewCopycat : ScrollView {     private readonly Action<Size> setContentSize;      public ScrollViewCopycat()     {         var methodInfo = typeof(ScrollViewCopycat)             .GetProperty("ContentSize", BindingFlags.Instance | BindingFlags.Public)             .GetSetMethod(true);         setContentSize = value => methodInfo.Invoke(this, new object[] { value });     }      protected override void LayoutChildren(double x, double y, double width, double height)     {     //[...]     setContentSize(new Size(width, Content.Bounds.Bottom + Padding.Bottom));     } } 

В какой-то момент нас окончательно добил следующий баг: при изменении значения свойства видимости для пачки элементов управления (выставляли для нескольких полей на экране свойство IsVisible, одним в False, другим в True) элемент мог просто не появиться на экране! При этом он занимал своё месту в иерархии (в форме на экране появлялась дыра), но реально он оказался скрыт. Проблема возникала не только у нас, можно найти несколько обсуждений на форуме Xamarin — вот примеры раз или два.

Баг оказался плавающим, причем появился он в Xamarin.Forms 1.3.3.6323 и более поздних, проблема возникала из-за состояния гонки внутри самих Forms. Поэтому мы некотороые время оставались на более старой, но зато не имеющией этого бага версии — 1.3.1.6296. К сожалению в этой версии тоже имелись свои баги, исправленные в более поздних.

Так что в конце концов мы пришли к таком решению:

  • у всех UI-контроллах, свойства которых мы хотим изменить, вызывается метод BatchBegin();
  • меняем необходимые свойства;
  • опять таки на всех контроллах вызываем BatchCommit().

Подробный код

public class Batch {     private readonly ILayoutController visualElement;      public Batch(ILayoutController visualElement)     {         this.visualElement = visualElement;     }      public IDisposable Begin()     {         var animatables = GatherAnimatables(visualElement).ToArray();         foreach (var animatable in animatables)             animatable.BatchBegin();          return new ActionDisposable(() =>                                     {                                         foreach (var animatable in animatables)                                             animatable.BatchCommit();                                     });     }      private static IEnumerable<IAnimatable> GatherAnimatables(ILayoutController root)     {         return root.Children.OfType<IAnimatable>()                     .Concat(root.Children.OfType<ILayoutController>().SelectMany(GatherAnimatables));     } }  

Данный код не только решает упомянутую проблему, но и является рекомендуемым при изменении нескольких свойств компонента сразу. Скажем, если код написан так:

if (alert)     {         errorlabel.IsVibislbe = true;         errorlabel.TextColor = Colors.Red;         errorlabel.Text = AlertText;     } 

То компонент будет перерисован трижды, после каждого изменения свойства. А вот если обернуть его в BatchBegin/BatchCommit — перерисовка (и пересчёт размера) произойдёт только один раз, что позитивно скажется на скорости.

Бывают и другие баги, например, TextView может повлиять на размер своего контейнера, хотя у того выставлен параметр «растягиваться на всю ширину»:

Возникает это, если вертикальный контейнер лежит в другом контейнере с горизонтальной ориентацией.

Код, приводящий к проблеме.

Content=new StackLayout {     Orientation = Orientation.Horizontal,     BackgroundColor = Color.Green,     Children =               {                   new StackLayout                   {                       Orientation = StackOrientation.Vertical,                       VerticalOptions = LayoutOptions.FillAndExpand,                       HorizontalOptions = LayoutOptions.FillAndExpand,                       Children =                       {                           new Label                           {                               BackgroundColor = Color.Red,                               HorizontalOptions = LayoutOptions.FillAndExpand,                           }                       }                   }               } } 

Связь моделей и UI-компонентов (биндинг)

Встроенная поддержка двухстороннего биндинга между моделью и вьюшкой нас тоже не порадовала. Вот первый вариант указания связи:

public class Model1 {     public string Text { get; private set; }     public Model1 (string text)     {         Text = text;     } }  var label1 = new Label {     BindingContext = new Model1("Hello, problems!") }  label1.SetBinding(Label.TextProperty, "Text"); 

Если ошибиться, и вместо “Text” написать другое имя — то ни на этапе компиляции, ни в рантайме ничего не взорвётся. Просто Label отобразится без текста.

Есть конечно чуть лучший вариант установки связи:

label1.SetBinding<Model1>(Label.TextProperty, source => source.Text); 

Но и он не спасает нас от ситуации, когда в Label будет помещён другой объект:

var label1 = new Label {     BindingContext = new Model2(), }; 

В этом случае опять таки ничего при выполнении не упадёт.

Но и это ещё не всё. Если вам нужны взаимосвязанные поля в модели (когда при изменении одного изменяется и другое) — для работы UI придётся дописать немного довольно скучного кода — реализовать интерфейс INotifyPropertyChanged и самостоятельно сообщать список изменившихся полей:

public class Model : INotifyPropertyChanged {     public event PropertyChangedEventHandler PropertyChanged;      private void OnPropertyChanged([CallerMemberName]string propertyName = null)     {         if (PropertyChanged != null)             PropertyChanged(this, new PropertyChangedEventArgs(propertyName));     }      private int value1;     public int Value1     {         get { return value1; }         set         {             value1 = value;             OnPropertyChanged();             OnPropertyChanged("Value2");         }     }      public int Value2     {         get { return Value1*2; }     } } 

По этим причинам биндинг между моделью и контроллами мы написали свой — проверяющий соответствие типов полей, автоматически обновляющий связанные поля и т.п.

Списки

Ну и отдельная головная боль — списки. Начнём с мелочей: у списка есть заголовок и подвал (footer и header), этакие уникальные ячейки, которые прокручиваются вместе с обычными строчками. Это хорошо. Но при замене контента заголовка тот не пересчитывает свою высоту, если новый заголовок больше или меньше предшественника, а высота строк таблицы зафиксирована. Приходится делать это вручную

public interface IHeader {     Layout GetView();     double GetHeight(); }  public void SetHeaderForm(IHeader value) {     value.GetView().Layout(new Rectangle(value.GetView().X, value.GetView().Y, Width, value.GetHeight()));     list.Header = value; } 

Если писать на нативных iOS компонентах — такой проблемы не возникнет, размер пересчитается сам.

Другой неприятный момент – “контекстные действия”. Это меню как правило вызывается на Android долгим тапом, а на iOS – свайпом по ячейке. Неприятность ситуации в том, что для этих контекстных действий в Xamarin.Forms используется объект MenuItem, имеющий среди всего прочего свойство Icon. Но в данных менюшках никакие иконки не отображаются. И это фича.

Так что для показа иконок мы задействовали Object-C библиотеку MGSwipeTableCell, вокруг которой написали свою обёртку. Правда в результате мы потеряли возможность автоматического изменения размера ячеек в списке – все они теперь должны быть строго одной высоты, т.к написание корректного сложного кастомного рендера ячейки не так просто, как кажется.

Ну и напоследок, хотя список в качестве источника данных принимае IEnumerable, “подгрузки по мере прокрутки” по-умолчанию нет — в момент определения источника компонент вычитывает данные до конца. Не то что бы мы сильно ждали подобного поведения, т.к.«из коробки» бесконечных списков нет ни в iOS ни в Android, но лёгкая надежда всё-таки была. Увы, компоненты Xamarin.Forms реализуют исключительно прожиточный минимум возможностей — всё остальное придётся дописывать самим.

Выводы

Стоит или нет использовать Xamarin.Forms – нам покажет следующий этап, перенос уже написанного под Android Java-проекта на Forms. Но уже сейчас мы можем сказать, что Xamarin.Forms стоит использовать только для максимально простого UI. Если в планах есть использование всех до единой фишек конкретной платформы или хитрые дизайнерские решения – Xamarin.Forms будет больше мешать, чем помогать. В этом варианте лучше использовать Xamarin исключительно для бизнес-логики, а вёрстку для каждой из платформ делать нативной.

Если у вас остались вопросы или есть замечания — с удовольствием ответим на них в комментариях.

ссылка на оригинал статьи http://habrahabr.ru/post/273161/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *