Главная » Хабрахабр » Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 2)

Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 2)

Это вторая статья из цикла, где мы проводим сравнение Kivy, Xamarin.Forms и React Native. В ней я постараюсь написать такой же планировщик задач, но с использованием Xamarin.Forms. Посмотрю, как у меня это получится, и с чем мне придется столкнуться.

Xamarin. Повторять ТЗ я не буду, его можно посмотреть в первой статье: Kivy. Три фреймворка — один эксперемент
Для начала скажу пару слов о платформе Xamarin. React Native. Xamarin. Forms и о том, как я буду подходить к решению поставленной задачи. Android. Forms является надстройкой над Xamarin.iOs и Xamarin. После сборки общая часть “разворачивается” в стандартные нативные контролы, так что по сути вы получаете полностью нативные приложения под все поддерживаемые платформы.

Forms крайне близок к синтаксису WPF, а сама общая часть написана на . Синтаксис Xamarin. В результате вы получаете возможность использования MVVM подхода при разработке приложения, а также доступ к огромному количеству сторонних библиотек, написанных для . NET Standard. Forms приложениях. NET Standard и уже лежащих в NuGet, которые вы спокойно можете использовать у себя в Xamarin.

Исходные коды приведённого здесь приложения доступны на GitHub.

Forms приложение и начнём. Итак, давайте создадим пустое Xamarin. Модель данных у нас будет простая, всего два класса Note и Project:

public class Note public string UserName { get; set; } public DateTime EditTime { get; set; } public string Text { get; set; }
} public class Project { public string Name { get; set; } public ObservableCollection<Note> Notes { get; set; } public Project() { Notes = new ObservableCollection<Note>(); }
}

Я буду стараться придерживаться MVVM подхода, но какую-то специальную библиотеку использовать не буду, чтобы не усложнять код. Все классы моделей и вью моделей будут реализовывать интерфейс INotifyPropertyChanged. Его реализацию в приводимых примерах кода я уберу для лаконичности.

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

public class MainViewModel { public ObservableCollection<Project> Projects { get; set; } public MainViewModel() { Projects = Project.GetTestProjects(); } public void AddNewProject(string name) { Project project = new Project() { Name = name }; Projects.Add(project); } public void DeleteProject(Project project) { Projects.Remove(project); }
}

Код самого экрана:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:TodoList.View" x:Class="TodoList.View.ProjectsPage"> <ContentPage.ToolbarItems> <ToolbarItem Clicked="AddNew_Clicked" Icon="plus.png"/> </ContentPage.ToolbarItems> <ListView ItemsSource="{Binding Projects}" ItemTapped="List_ItemTapped"> <ListView.ItemTemplate> <DataTemplate> <TextCell Text="{Binding Name}" TextColor="Black"> <TextCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </TextCell.ContextActions> </TextCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
</ContentPage>

Разметка получилась достаточно простая, единственное, на чем хочется остановиться — это реализация свайп-кнопок для удаления проектов. В ListView есть понятие ContextActions, если его задать, то в iOS они будут реализованы через свайп, в Android — через длинный тап. Данный подход реализован в Xamarin.Forms, ибо он является нативным для каждой из платформ. Однако если мы захотим свайп в андроиде, нам надо будет руками реализовывать его в нативной части андроида. У меня нет задачи тратить много времени на это, поэтому я удовлетворился стандартным подходом 🙂 В результате свайп в iOS и контекстное меню в Android реализуются достаточно просто:

<TextCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/>
</TextCell.ContextActions>

Подставив тестовые данные, получаем вот такой список:

Начнём с простого — удаления проекта: Теперь перейдём к обработчику событий.

MainViewModel ViewModel { get { return BindingContext as MainViewModel; } } async Task DeleteItem_Clicked(object sender, EventArgs e) { MenuItem menuItem = sender as MenuItem; if (menuItem == null) return; Project project = menuItem.CommandParameter as Project; if (project == null) return; bool answer = await DisplayAlert("Are you sure?", string.Format("Would you like to remove the {0} project", project.Name), "Yes", "No"); if(answer) ViewModel.DeleteProject(project);
}

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

На Android будет свой вариант подобного окна. Данное окошко из iOs.

Казалось бы, это делается по аналогии, но в Xamarin. Следующим реализуем добавление нового проекта. Варианта решения есть два: Forms нет реализации диалога, подобного тому, которым я подтверждал удаление, но позволяющего вводить текст.

  • написать свой сервис, который будет поднимать нативные диалоги;
  • реализовать какой-то воркэраунд на стороне Xamarin.Forms.

Мне не хотелось тратить время на поднятие диалога через натив, и я решил воспользоваться вторым подходом, реализацию которого взял из треда: How to do a simple InputBox dialog?, а именно метод Task InputBox(INavigation navigation).

async Task AddNew_Clicked(object sender, EventArgs e) { string result = await InputBox(this.Navigation); if (result == null) return; ViewModel.AddNewProject(result);
}

Теперь обработаем тап по строкам, для открытия проекта:

void List_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e) { Project project = e.Item as Project; if (project == null) return; this.Navigation.PushAsync(new NotesPage() { BindingContext = new ProjectViewModel(project) });
}

Как видно из кода выше, чтобы перейти на окно проекта, нам нужны его view model и объект page окна.

Свойство Navigation определяется в VisualElement class, и позволяет работать с навигационной панелью в любой view вашего приложения без прокидывания её туда руками. Хотелось бы сказать пару слов про Navigation. Поэтому в App.xaml.cs напишем: Однако, чтобы этот подход работал, создать данную панель всё-таки надо самому.

NavigationPage navigation = new NavigationPage();
navigation.PushAsync(new View.ProjectsPage() { BindingContext = new MainViewModel() });
MainPage = navigation;

Где ProjectsPage — это как раз то окно, которое я сейчас описываю.

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

Разметка этого окна получилась посложнее, ибо отображать каждая строка должна больше информации:

Notes View

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="TodoList.View.NotesPage" xmlns:local="clr-namespace:TodoList.View" xmlns:utils="clr-namespace:TodoList.Utils" Title="{Binding Project.Name}"> <ContentPage.Resources> <ResourceDictionary> <utils:PathToImageConverter x:Key="PathToImageConverter"/> </ResourceDictionary> </ContentPage.Resources> <ContentPage.ToolbarItems> <ToolbarItem Clicked="AddNew_Clicked" Icon="plus.png"/> </ContentPage.ToolbarItems> <ListView ItemsSource="{Binding Project.Notes}" x:Name="list" ItemTapped="List_ItemTapped" HasUnevenRows="True"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <local:MyCellGrid Margin="5"> <local:MyCellGrid.RowDefinitions> <RowDefinition Height="40"/> <RowDefinition Height="*"/> </local:MyCellGrid.RowDefinitions> <local:MyCellGrid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="40"/> </local:MyCellGrid.ColumnDefinitions> <Image Grid.Row="0" Grid.Column="0" Source="{Binding UserIconPath, Converter={StaticResource PathToImageConverter}}" /> <StackLayout Grid.Row="0" Grid.Column="1"> <Label Text="{Binding UserName}" FontAttributes="Bold"/> <Label Text="{Binding EditTime}"/> </StackLayout> <Button Grid.Row="0" Grid.Column="2" BackgroundColor="Transparent" Image="menu.png" Margin="5" HorizontalOptions="FillAndExpand" Clicked="RowMenu_Clicked"/> <local:MyLabel Grid.Row="1" Grid.Column="1" Margin="0,10,0,0" Grid.ColumnSpan="2" Text="{Binding Text}"/> </local:MyCellGrid> <ViewCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </ViewCell.ContextActions> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
</ContentPage>

В контенте окна у нас опять лежит ListView, прибинженная к коллекции заметок. Однако мы хотим высоту ячеек по контенту, но не более 150, для этого выставим HasUnevenRows=«True», чтобы ListView позволил ячейкам занимать столько места, сколько они попросят. Но в такой ситуации строки могут запросить высоту более 150 и ListView им позволит так отобразиться. Чтобы этого избежать в ячейке я использовал своего наследника Grid панели: MyCellGrid. Данная панель на операции measure запрашивает высоту внутренних элементов и возвращает ее либо 150, если она больше:

public class MyCellGrid : Grid { protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { SizeRequest sizeRequest = base.OnMeasure(widthConstraint, heightConstraint); if (sizeRequest.Request.Height <= 150) return sizeRequest; return new SizeRequest(new Size() { Width = sizeRequest.Request.Width, Height = 150 }); }
}

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

<Button Grid.Row="0" Grid.Column="2" BackgroundColor="Transparent" Image="menu.png" Margin="5" HorizontalOptions="FillAndExpand" Clicked="RowMenu_Clicked"/>

С тестовыми данными наша форма выглядит вот так:

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

Действительно, нам надо всего лишь создать view примерно такого вида:

<StackLayout> <Button Text=”Edit”/> <Button Text=”Delete”/>
</StackLayout>

И показывать её рядом с кнопкой. Однако проблема в том, что мы не можем точно узнать, где это “рядом с кнопкой”. Данное контекстное меню должно быть расположено поверх ListView и, при открытии, позиционироваться в координатах окна. Для этого надо знать координаты нажатой кнопки относительно окна. Мы же можем получить координаты кнопки только относительно внутреннего ScrollView, расположенного в ListView. Так что когда строки не сдвинуты, то все нормально, но когда строки проскроллированы, мы должны учитывать то, на сколько произошел скролл при расчете координат. ListView нам не отдает величину скролла. Так что его надо вытягивать из натива, что делать очень не хотелось. Поэтому я решил пойти по пути более стандартному и простому: показать стандартное системное контекстное меню. В результате обработчик нажатия на кнопку получится следующий:

async Task RowMenu_Clicked(object sender, System.EventArgs e) { string action = await DisplayActionSheet("Note action:", "Cancel", null, "Edit", "Delete"); if (action == null) return; BindableObject bindableSender = sender as BindableObject; if(bindableSender != null) { Note note = bindableSender.BindingContext as Note; if (action == "Edit") { EditNote(note); } else if(action == "Delete") { await DeleteNote(note); } }
}

Вызов метода DisplayActionSheet как раз и показывает штатное контекстное меню:

Это сделано вот для чего. Если вы заметили, текст заметки у меня выводится в моем контроле MyLabel, а не в штатном Label. Однако Xamarin. Когда пользователь изменяет текст заметки, срабатывает биндинг, и в Label автоматически прилетает новый текст. Разработчики Xamarin заявляют, что это достаточно дорогостоящая операция. Forms не пересчитывает размер ячейки при этом. Единственное, что у них для этого есть, это метод ForceUpdateSize у объекта Cell. Да и у самого ListView нет какого-то метода, который заставил бы его пересчитать свой размер, InvalidateLayout тоже не помогает. Поэтому, чтобы до него добраться и в нужный момент дёрнуть, я написал свой наследник Label и дёргаю этот метод на каждое изменение текста:

public class MyLabel : Label { protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) { base.OnPropertyChanged(propertyName); if (propertyName == "Text") { ((this.Parent as MyCellGrid).Parent as Cell).ForceUpdateSize(); } }
}

Теперь после редактирования заметки ListView автоматически поправит размер ячейки под новый текст.

При редактировании или создании новой заметки открывается окно с Editor в контенте и кнопкой Save на тулбаре:

Если её расположить просто поверх редактора, то она будет перекрыта выезжающей клавиатурой. Данное окно немного отличается от того, что у нас в ТЗ: отсутствием круглой кнопки снизу. Поэтому убрал её и оставил только кнопку Save в верхней панели. Красивого решения как её подвинуть и не уходить при этом в натив быстрым поиском я не нашёл. Само по себе данное окно очень простое, так что его описание я опущу.

Что хочется сказать в итоге.

Forms хорошо подойдёт тем, кто хорошо знаком с инфраструктурой . Xamarin. Им не придётся переходить на новые IDE и фреймворки. NET и давно с ней работает. К тому же Xamarin позволяет разрабатывать и билдить iOS приложения в Visual Studio под Windows. Как видно, код приложения мало чем отличается от кода любого другого XAML based приложения.

Forms, вам не надо никакого красноглазия с консолью. Для того чтобы начать писать приложения на Xamarin. Обо всём остальном за вас уже позаботились. Просто ставите Visual Studio и пишете приложения. При этом, как бы Microsoft не ассоциировался с платными продуктами, Xamarin бесплатен и есть бесплатные версии Visual Studio.

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

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

Однако, конечно же, не обошлось и без недостатков.

Например, как видно в моём примере, все платформы содержат alert-сообщения и контекстные меню, и эта вещь доступна в Xamarin. API, доступное в общей части, достаточно скудное, ибо содержит в себе только то, что является общим для всех платформ. Однако стандартное меню, позволяющее ввести текст, доступно лишь в iOS, поэтому в Xamarin. Forms. Forms его нет.

Что-то сделать можно, что-то нельзя. Так же подобные ограничения встречаются и в использовании компонентов. В Android данный context action будет представлен в виде меню, показывающемся на длинном тапе. Тот же свайп для удаления проекта или заметки работает лишь в iOS. А если хочется свайп в андроиде, то welcome в андроид часть и писать это руками.

Скорость работы приложения на Xamarin. Ну и конечно же производительность. Так что сам Microsoft заявляет, что если вам надо приложение без особых изысков в плане дизайна и требований к производительности, то Xamarin. Forms в любом случае будет ниже скорости работы нативного приложения. Если нужны красивости или скорость, то тут надо уже опускаться в натив. Forms для вас. Благо Xamarin имеет версии и под натив, которые уже оперируют сразу родным платформенным API и работают быстрее, чем формсы.


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

HighLoad++ Awards: награда, которую деплоили, деплоили и наконец задеплоили

Например, придумали механику asset pipeline на Rails, и в 2011–2012 году о ней начали выходить статьи в американских блогах, о ней раструбили по всему миру. — На рынке очень грустная ситуация в отношении того, насколько мы, программисты, умеем делиться знаниями ...

Youtube-party: История компьютерных игр

В смартфонах мы играем в такие игры, о которых лет 15–20 назад не мечтали даже владельцы мощных настольных компьютеров. Сегодня можно выбрать себе игры на любой вкус, с каждым годом их выходит всё больше. В детстве/юности/молодости мы фанатели от игр, ...