Kivy. Xamarin React Native. Trois cadres - une expérience (partie 2)


Il s'agit du deuxième article d'une série où nous comparons Kivy, Xamarin.Forms et React Native. Dans ce document, je vais essayer d'écrire le même planificateur de tâches, mais en utilisant Xamarin.Forms. Je vais voir comment je le fais et à quoi je dois faire face.

Je ne répéterai pas les savoirs traditionnels, comme on peut le voir dans le premier article: Kivy. Xamarin React Native. Trois cadres - une expérience

La troisième partie concerne React Native: Kivy. Xamarin React Native. Trois cadres - une expérience (partie 3)

Pour commencer, je vais dire quelques mots sur la plate-forme Xamarin.Forms et comment j'aborderai la solution de la tâche. Xamarin.Forms est un module complémentaire pour Xamarin.iOs et Xamarin.Android. Après l'assemblage, la partie générale est «déployée» sur des contrôles natifs standard, vous obtenez donc essentiellement des applications entièrement natives pour toutes les plates-formes prises en charge.

La syntaxe de Xamarin.Forms est très proche de la syntaxe de WPF, et la partie générale est écrite en .NET Standard. Par conséquent, vous avez la possibilité d'utiliser l'approche MVVM lors du développement de l'application, ainsi que l'accès à un grand nombre de bibliothèques tierces écrites pour .NET Standard et déjà dans NuGet, que vous pouvez utiliser en toute sécurité dans vos applications Xamarin.Forms.

Le code source de l'application ici est disponible sur GitHub .

Créons donc une application Xamarin.Forms vide et commençons. Nous aurons un modèle de données simple, avec seulement deux classes Note et Project:

public class Note { public string UserIconPath { get; set; } 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>(); } } 

J'essaierai d'adhérer à l'approche MVVM, mais je n'utiliserai aucune bibliothèque spéciale, afin de ne pas compliquer le code. Toutes les classes de modèle et les modèles de vue implémenteront l'interface INotifyPropertyChanged. Je vais supprimer son implémentation dans les exemples de code donnés pour plus de brièveté.

Le premier écran sera une liste de projets avec la possibilité d'en créer un nouveau ou de supprimer l'actuel. Faisons un modèle pour lui:

 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); } } 

Code d'écran lui-même:

 <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> 

Le balisage s'est avéré assez simple, la seule chose sur laquelle je veux m'attarder est la mise en œuvre de boutons de balayage pour supprimer des projets. Dans ListView, il y a le concept de ContextActions, si vous le définissez, alors dans iOS, elles seront mises en œuvre par glissement, dans Android - via un long tapotement. Cette approche est implémentée dans Xamarin.Forms, car elle est native pour chaque plate-forme. Cependant, si nous voulons glisser dans l'androïde, nous devrons l'implémenter avec nos mains dans la partie native de l'androïde. Je n'ai pas de tâche à consacrer beaucoup de temps à cela, alors j'étais satisfait de l'approche standard :) Par conséquent, le balayage dans iOS et le menu contextuel dans Android sont mis en œuvre tout simplement:

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

En remplaçant les données de test, nous obtenons la liste suivante:



Passons maintenant au gestionnaire d'événements. Commençons par un simple - supprimer un projet:

 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); } 

Il n'est pas bon de supprimer quelque chose sans demander à l'utilisateur, et dans Xamarin.Forms, il est facile de le faire en utilisant la méthode DisplayAlert standard. Après l'avoir appelé, la fenêtre suivante apparaîtra:



Cette fenêtre provient d'iOs. Android aura sa propre version d'une fenêtre similaire.

Ensuite, nous mettrons en œuvre l'ajout d'un nouveau projet. Il semblerait que cela se fasse par analogie, mais dans Xamarin.Forms, il n'y a pas de mise en œuvre d'un dialogue similaire à celui que j'ai confirmé la suppression, mais vous permettant de saisir du texte. Il existe deux solutions possibles:

  • écrire votre propre service qui déclenchera des dialogues natifs;
  • implémenter une sorte de solution de contournement du côté de Xamarin.Forms.

Je ne voulais pas perdre de temps à élever le dialogue via le natif, et j'ai décidé d'utiliser la deuxième approche, dont la mise en œuvre a été tirée du fil: Comment faire une simple boîte de dialogue InputBox? , à savoir la méthode Task InputBox (navigation INavigation).

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

Maintenant, nous allons traiter tap by row, pour ouvrir le projet:

 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) }); } 

Comme vous pouvez le voir dans le code ci-dessus, pour accéder à la fenêtre du projet, nous avons besoin de son modèle de vue et de l'objet page de la fenêtre.

Je voudrais dire quelques mots sur la navigation. La propriété Navigation est définie dans la classe VisualElement et vous permet de travailler avec le panneau de navigation dans n'importe quelle vue de votre application sans avoir à la faire rouler avec vos mains. Cependant, pour que cette approche fonctionne, vous devez toujours créer ce panneau vous-même. Par conséquent, dans App.xaml.cs, nous écrivons:

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

Où ProjectsPage est exactement la fenêtre que je décris maintenant.

La fenêtre avec des notes est très similaire à la fenêtre avec des projets, donc je ne la décrirai pas en détail, je me concentrerai uniquement sur des nuances intéressantes.

La disposition de cette fenêtre s'est avérée plus compliquée, car chaque ligne devrait afficher plus d'informations:

Vue Notes
 <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> 


Dans le contenu de la fenêtre, nous avons à nouveau un ListView, attaché à la collection de notes. Cependant, nous voulons la hauteur des cellules dans le contenu, mais pas plus de 150, pour cela, nous définissons HasUnevenRows = "True" afin que ListView permette aux cellules de prendre autant d'espace qu'elles le demandent. Mais dans cette situation, les lignes peuvent demander une hauteur de plus de 150 et ListView leur permettra d'être affichées comme ceci. Pour éviter cela dans la cellule, j'ai utilisé mon héritier du panneau Grille: MyCellGrid. Ce panneau sur l'opération de mesure demande la hauteur des éléments internes et la renvoie soit 150 si elle est supérieure:

 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 }); } } 

Étant donné que selon le TOR, nous devons être en mesure d'éditer et de supprimer, en plus d'appuyer et de glisser, également le menu qui s'ouvre en cliquant sur le bouton dans le coin de la ligne, nous ajouterons ce bouton au modèle de cellule et nous abonnerons au robinet. Dans ce cas, si l'utilisateur clique sur le bouton, alors il intercepte le geste et nous ne recevrons aucun événement de clic sur la ligne.

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

Avec les données de test, notre formulaire ressemble à ceci:



Le traitement des actions des utilisateurs dans ce formulaire est complètement analogue à celui qui a été écrit pour la fenêtre de liste de projets. Je veux m'arrêter uniquement sur le menu contextuel par notre bouton dans le coin de la ligne. Au début, je pensais que je le ferais au niveau Xamarin.Forms sans aucun problème.

En effet, nous avons juste besoin de créer une vue de quelque chose comme ça:

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

Et affichez-le à côté du bouton. Cependant, le problème est que nous ne pouvons pas savoir exactement où il se trouve «à côté du bouton». Ce menu contextuel doit être situé au-dessus de ListView et, une fois ouvert, positionné dans les coordonnées de la fenêtre. Pour ce faire, vous devez connaître les coordonnées du bouton enfoncé par rapport à la fenêtre. Nous pouvons obtenir les coordonnées du bouton uniquement par rapport au ScrollView interne situé dans le ListView. Donc, lorsque les lignes ne sont pas décalées, tout va bien, mais lorsque les lignes défilent, nous devons prendre en compte la quantité de défilement qui s'est produite lors du calcul des coordonnées. ListView ne nous donne pas la valeur de défilement. Il faut donc le retirer du natif, ce que je ne voulais vraiment pas faire. Par conséquent, j'ai décidé de prendre le chemin d'un chemin plus standard et plus simple: afficher le menu contextuel du système standard. Par conséquent, le gestionnaire de clic de bouton se révélera comme suit:

 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); } } } 

L'appel de la méthode DisplayActionSheet affiche simplement le menu contextuel normal:



Si vous remarquez, le texte de la note s'affiche dans mon contrôle MyLabel, et non dans l'étiquette régulière. C'est fait pour quoi. Lorsque l'utilisateur modifie le texte de la note, des classeurs sont déclenchés et un nouveau texte arrive automatiquement dans Label. Cependant, Xamarin.Forms ne recalcule pas la taille de cellule en même temps. Les développeurs de Xamarin affirment qu'il s'agit d'une opération assez coûteuse. Oui, et ListView lui-même n'a aucune méthode qui lui ferait recompter sa taille, InvalidateLayout n'aide pas non plus. La seule chose qu'ils ont pour cela est la méthode ForceUpdateSize de l'objet Cell. Par conséquent, afin d'y accéder et de tirer au bon moment, j'ai écrit mon héritier Label et je tire cette méthode pour chaque changement de texte:

 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(); } } } 

Maintenant, après avoir modifié une note, ListView ajustera automatiquement la taille de la cellule pour s'adapter au nouveau texte.

Lors de l'édition ou de la création d'une nouvelle note, une fenêtre s'ouvre avec l'éditeur dans le contenu et le bouton Enregistrer de la barre d'outils:



Cette fenêtre est légèrement différente de ce que nous avons dans le TK: l'absence d'un bouton rond en bas. Si vous le placez simplement au-dessus de l'éditeur, il sera bloqué par le clavier sortant. En même temps, je n'ai pas trouvé de belle solution pour le déplacer et ne pas entrer dans le natif avec une recherche rapide. Par conséquent, je l'ai supprimé et je n'ai laissé que le bouton Enregistrer dans le panneau supérieur. Cette fenêtre elle-même est très simple, je vais donc omettre sa description.

Ce que je veux dire à la fin.

Xamarin.Forms convient parfaitement à ceux qui connaissent bien l'infrastructure .NET et qui travaillent avec elle depuis longtemps. Ils n'auront pas à mettre à niveau vers de nouveaux IDE et frameworks. Comme vous pouvez le voir, le code d'application n'est pas très différent du code de toute autre application basée sur XAML. De plus, Xamarin vous permet de développer et de créer des applications iOS dans Visual Studio pour Windows. Lors du développement de l'application finale pour le tester et la créer, vous devrez vous connecter à une machine MacOS. Et les bibliothèques peuvent se faire sans.

Pour commencer à écrire des applications sur Xamarin.Forms, vous n'avez pas besoin de yeux rouges avec la console. Installez simplement Visual Studio et écrivez des applications. Tout le reste a déjà été pris en charge pour vous. Dans le même temps, peu importe la façon dont Microsoft est associé aux produits payants, Xamarin est gratuit et il existe des versions gratuites de Visual Studio.

Le fait que Xamarin.Forms utilise la norme .NET sous le capot vous donne accès à un tas de bibliothèques déjà écrites pour cela qui vous faciliteront la vie lors du développement de vos applications.

Xamarin.Forms vous permet d'ajouter facilement quelque chose dans les parties natives de votre application, si vous souhaitez implémenter quelque chose de spécifique à la plate-forme. Là, vous obtenez le même C #, mais l'API est native pour chaque plate-forme.

Cependant, bien sûr, il y avait quelques lacunes.

L'API, disponible dans la partie générale, est plutôt maigre, car elle ne contient que ce qui est commun à toutes les plateformes. Par exemple, comme on peut le voir dans mon exemple, toutes les plateformes contiennent des messages d'alerte et des menus contextuels, et cette chose est disponible dans Xamarin.Forms. Cependant, le menu standard qui vous permet de saisir du texte n'est disponible que dans iOS, donc dans Xamarin.Forms, il ne l'est pas.

Des restrictions similaires se retrouvent dans l'utilisation des composants. Quelque chose peut être fait, quelque chose est impossible. Le même glissement pour supprimer un projet ou une note ne fonctionne que sur iOS. Dans Android, cette action contextuelle sera présentée comme un menu qui apparaît sur un long tap. Et si vous voulez glisser dans l'androïde, alors bienvenue dans la partie android et écrivez-le avec vos mains.

Et bien sûr, la performance. La vitesse de l'application sur Xamarin.Forms sera en tout cas inférieure à la vitesse de l'application native. Microsoft dit donc que si vous avez besoin d'une application sans fioritures en termes d'exigences de conception et de performances, Xamarin.Forms est fait pour vous. Si vous avez besoin de joliesse ou de vitesse, alors vous devriez déjà descendre au natif. Heureusement, Xamarin dispose également de versions natives qui fonctionnent déjà immédiatement avec leurs API de plateforme natives et fonctionnent plus rapidement que les formulaires.

Source: https://habr.com/ru/post/fr420999/


All Articles