Kivy Xamarin Reaccionar nativo. Tres marcos: un experimento (parte 2)


Este es el segundo artículo de una serie en el que comparamos Kivy, Xamarin.Forms y React Native. En él, intentaré escribir el mismo programador de tareas, pero usando Xamarin.Forms. Veré cómo lo hago y lo que tengo que enfrentar.

No repetiré TK; se puede ver en el primer artículo: Kivy. Xamarin Reaccionar nativo. Tres marcos: un experimento

La tercera parte trata sobre React Native: Kivy. Xamarin Reaccionar nativo. Tres marcos: un experimento (parte 3)

Para comenzar, diré algunas palabras sobre la plataforma Xamarin.Forms y cómo abordaré la solución de la tarea. Xamarin.Forms es un complemento para Xamarin.iOs y Xamarin.Android. Después del ensamblaje, la parte general se "implementa" en los controles nativos estándar, por lo que, en esencia, se obtienen aplicaciones completamente nativas para todas las plataformas compatibles.

La sintaxis de Xamarin.Forms está muy cerca de la sintaxis de WPF, y la parte general está escrita en .NET Standard. Como resultado, tiene la oportunidad de utilizar el enfoque MVVM al desarrollar la aplicación, así como acceder a una gran cantidad de bibliotecas de terceros escritas para .NET Standard y ya en NuGet, que puede usar de forma segura en sus aplicaciones Xamarin.Forms.

El código fuente de la aplicación aquí está disponible en GitHub .

Entonces, creemos una aplicación Xamarin.Forms vacía y comencemos. Tendremos un modelo de datos simple, con solo dos clases de Nota y Proyecto:

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

Intentaré adherirme al enfoque MVVM, pero no usaré ninguna biblioteca especial, para no complicar el código. Todas las clases de modelos y modelos de vista implementarán la interfaz INotifyPropertyChanged. Eliminaré su implementación en los ejemplos de código dados por brevedad.

La primera pantalla será una lista de proyectos con la capacidad de crear uno nuevo o eliminar el actual. Hagamos un modelo para él:

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

Código de pantalla en sí:

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

El diseño es bastante simple, lo único en lo que quiero detenerme es en la implementación de botones deslizantes para eliminar proyectos. En ListView existe el concepto de ContextActions, si lo configura, en iOS se implementará mediante deslizar, en Android, a través de un toque prolongado. Este enfoque se implementa en Xamarin.Forms, porque es nativo para cada plataforma. Sin embargo, si queremos deslizar el dedo en el Android, tendremos que implementarlo con nuestras manos en la parte nativa del Android. No tengo la tarea de dedicar mucho tiempo a esto, así que me quedé satisfecho con el enfoque estándar :) Como resultado, deslizar en iOS y el menú contextual en Android se implementan de manera bastante simple:

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

Sustituyendo los datos de prueba, obtenemos la siguiente lista:



Ahora pasemos al controlador de eventos. Comencemos con un simple: eliminar un proyecto:

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

No es bueno eliminar algo sin preguntarle al usuario, y en Xamarin.Forms es fácil hacerlo usando el método estándar DisplayAlert. Después de llamarlo, aparecerá la siguiente ventana:



Esta ventana es de iOs. Android tendrá su propia versión de una ventana similar.

A continuación implementaremos la adición de un nuevo proyecto. Parecería que esto se hace por analogía, pero en Xamarin.Forms no hay implementación de un diálogo similar al que confirme la eliminación, pero que le permite ingresar texto. Hay dos posibles soluciones:

  • escriba su propio servicio que genere diálogos nativos;
  • implementar algún tipo de solución alternativa al lado de Xamarin.Forms.

No quería perder el tiempo elevando el diálogo a través del nativo, y decidí usar el segundo enfoque, cuya implementación tomé del hilo: ¿Cómo hacer un simple diálogo de InputBox? , a saber, el método Task InputBox (navegación INavigation).

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

Ahora procesaremos toque por fila para abrir el proyecto:

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

Como puede ver en el código anterior, para ir a la ventana del proyecto, necesitamos su modelo de vista y el objeto de página de la ventana.

Me gustaría decir algunas palabras sobre la navegación. La propiedad de navegación se define en la clase VisualElement y le permite trabajar con el panel de navegación en cualquier vista de su aplicación sin tener que desplazarla con las manos. Sin embargo, para que este enfoque funcione, aún necesita crear este panel usted mismo. Por lo tanto, en App.xaml.cs escribimos:

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

Donde ProjectsPage es exactamente la ventana que ahora estoy describiendo.

La ventana con notas es muy similar a la ventana con proyectos, por lo que no la describiré en detalle, me centraré solo en matices interesantes.

El diseño de esta ventana resultó ser más complicado, porque cada línea debería mostrar más información:

Vista de notas
 <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> 


En el contenido de la ventana, nuevamente tenemos un ListView, adjunto a la colección de notas. Sin embargo, queremos la altura de las celdas en el contenido, pero no más de 150, para esto configuramos HasUnevenRows = "True" para que ListView permita que las celdas ocupen tanto espacio como lo soliciten. Pero en esta situación, las filas pueden solicitar una altura de más de 150 y el ListView les permitirá mostrarse así. Para evitar esto en la celda, utilicé a mi heredero del panel Grid: MyCellGrid. Este panel en la operación de medición solicita la altura de los elementos internos y la devuelve 150 si es mayor:

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

Dado que, de acuerdo con los TOR, debemos poder editar y eliminar, además de tocar y deslizar, también el menú que se abre al hacer clic en el botón en la esquina de la línea, agregaremos este botón a la plantilla de celda y nos suscribiremos al toque. En este caso, si el usuario hace clic en el botón, intercepta el gesto y no recibiremos eventos de hacer clic en la línea.

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

Con los datos de prueba, nuestro formulario se ve así:



El procesamiento de las acciones del usuario en este formulario es completamente análogo al que se escribió para la ventana de la lista de proyectos. Quiero detenerme solo en el menú contextual con nuestro botón en la esquina de la línea. Al principio, pensé que lo haría en el nivel de Xamarin.Forms sin ningún problema.

De hecho, solo necesitamos crear una vista de algo como esto:

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

Y muéstralo al lado del botón. Sin embargo, el problema es que no podemos saber exactamente dónde está "al lado del botón". Este menú contextual debe ubicarse en la parte superior de ListView y, cuando se abre, colocarse en las coordenadas de la ventana. Para hacer esto, debe conocer las coordenadas del botón presionado en relación con la ventana. Podemos obtener las coordenadas del botón solo en relación con el ScrollView interno ubicado en el ListView. Entonces, cuando las líneas no se desplazan, entonces todo está bien, pero cuando las líneas se desplazan, debemos tener en cuenta cuánto se produjo el desplazamiento al calcular las coordenadas. ListView no nos da el valor de desplazamiento. Por lo tanto, debe ser sacado del nativo, lo que realmente no quería hacer. Por lo tanto, decidí tomar el camino hacia uno más simple y estándar: mostrar el menú contextual del sistema estándar. Como resultado, el controlador de clic de botón tendrá el siguiente resultado:

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

Llamar al método DisplayActionSheet solo muestra el menú contextual normal:



Si observa, el texto de la nota se muestra en mi control MyLabel y no en la etiqueta normal. Esto se hace para qué. Cuando el usuario cambia el texto de la nota, se activan las carpetas y el texto nuevo llega automáticamente a la etiqueta. Sin embargo, Xamarin.Forms no vuelve a calcular el tamaño de celda al mismo tiempo. Los desarrolladores de Xamarin afirman que esta es una operación bastante costosa. Sí, y ListView en sí no tiene ningún método que lo haga contar su tamaño, InvalidateLayout tampoco ayuda. Lo único que tienen para esto es el método ForceUpdateSize del objeto Cell. Por lo tanto, para llegar a él y tirar en el momento adecuado, escribí mi heredero de Label y extraigo este método para cada cambio de texto:

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

Ahora, después de editar una nota, ListView ajustará automáticamente el tamaño de la celda para que se ajuste al nuevo texto.

Al editar o crear una nueva nota, se abre una ventana con el Editor en el contenido y el botón Guardar en la barra de herramientas:



Esta ventana es ligeramente diferente de lo que tenemos en el TK: la falta de un botón redondo en la parte inferior. Si lo coloca simplemente encima del editor, el teclado que lo abandona lo bloqueará. Al mismo tiempo, no encontré una solución hermosa para moverlo y no entrar en el nativo con una búsqueda rápida. Por lo tanto, lo eliminé y dejé solo el botón Guardar en el panel superior. Esta ventana en sí es muy simple, por lo que omitiré su descripción.

Lo que quiero decir al final.

Xamarin.Forms es muy adecuado para aquellos que están familiarizados con la infraestructura .NET y han estado trabajando con ella durante mucho tiempo. No tendrán que actualizarse a nuevos IDEs y marcos. Como puede ver, el código de la aplicación no es muy diferente del código de cualquier otra aplicación basada en XAML. Además, Xamarin le permite desarrollar y construir aplicaciones iOS en Visual Studio para Windows. Cuando desarrolle la aplicación final para probarla y compilarla, necesitará conectarse a una máquina MacOS. Y las bibliotecas se pueden hacer sin él.

Para comenzar a escribir aplicaciones en Xamarin.Forms, no necesita ojos rojos con la consola. Simplemente instale Visual Studio y escriba aplicaciones. Todo lo demás ya se ha ocupado de ti. Al mismo tiempo, no importa cómo se asocie Microsoft con los productos pagos, Xamarin es gratuito y hay versiones gratuitas de Visual Studio.

El hecho de que Xamarin.Forms use el estándar .NET bajo el capó le da acceso a un montón de bibliotecas ya escritas para él que le facilitarán la vida al desarrollar sus aplicaciones.

Xamarin.Forms le permite agregar fácilmente algo en las partes nativas de su aplicación, si desea implementar algo específico de la plataforma. Allí obtienes el mismo C #, pero la API es nativa de cada plataforma.

Sin embargo, por supuesto, hubo algunas deficiencias.

La API, disponible en la parte general, es bastante escasa, porque contiene solo lo que es común a todas las plataformas. Por ejemplo, como se puede ver en mi ejemplo, todas las plataformas contienen mensajes de alerta y menús contextuales, y esto está disponible en Xamarin.Forms. Sin embargo, el menú estándar que le permite ingresar texto está disponible solo en iOS, por lo que en Xamarin.Forms no lo está.

Se encuentran restricciones similares en el uso de componentes. Se puede hacer algo, algo es imposible. El mismo deslizamiento para eliminar un proyecto o nota solo funciona en iOS. En Android, esta acción contextual se presentará como un menú que aparece con un toque prolongado. Y si desea deslizar el dedo en el Android, bienvenido a la parte de Android y escríbalo con las manos.

Y, por supuesto, el rendimiento. La velocidad de la aplicación en Xamarin.Forms será, en cualquier caso, inferior a la velocidad de la aplicación nativa. Por lo tanto, Microsoft dice que si necesita una aplicación sin lujos en términos de diseño y requisitos de rendimiento, entonces Xamarin.Forms es para usted. Si necesitas belleza o velocidad, entonces ya deberías ir a lo nativo. Afortunadamente, Xamarin también tiene versiones nativas que ya operan inmediatamente con sus API de plataforma nativas y funcionan más rápido que los formularios.

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


All Articles