Kivy. Xamarin Reagir nativo. Três estruturas - um experimento (parte 2)


Este é o segundo artigo de uma série em que comparamos o Kivy, o Xamarin.Forms e o React Native. Nele, tentarei escrever o mesmo agendador de tarefas, mas usando o Xamarin.Forms. Vou ver como faço e o que tenho que enfrentar.

Não vou repetir TK, como pode ser visto no primeiro artigo: Kivy. Xamarin Reagir nativo. Três estruturas - um experimento

A terceira parte é sobre React Native: Kivy. Xamarin Reagir nativo. Três estruturas - um experimento (parte 3)

Primeiro, direi algumas palavras sobre a plataforma Xamarin.Forms e como abordarei a solução da tarefa. Xamarin.Forms é um complemento para o Xamarin.iOs e o Xamarin.Android. Após a montagem, a peça geral é "implantada" nos controles nativos padrão; portanto, você obtém aplicativos totalmente nativos para todas as plataformas suportadas.

A sintaxe do Xamarin.Forms está muito próxima da sintaxe do WPF e a parte geral é escrita no .NET Standard. Como resultado, você tem a oportunidade de usar a abordagem MVVM ao desenvolver o aplicativo, além de acessar um grande número de bibliotecas de terceiros criadas para o .NET Standard e já no NuGet, que você pode usar com segurança nos aplicativos Xamarin.Forms.

O código fonte do aplicativo aqui está disponível no GitHub .

Então, vamos criar um aplicativo Xamarin.Forms vazio e começar. Teremos um modelo de dados simples, com apenas duas classes Note e 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>(); } } 

Vou tentar aderir à abordagem MVVM, mas não usarei nenhuma biblioteca especial, para não complicar o código. Todas as classes de modelos e modelos de exibição implementarão a interface INotifyPropertyChanged. Eu removerei sua implementação nos exemplos de código fornecidos por questões de brevidade.

A primeira tela que teremos é uma lista de projetos com a capacidade de criar uma nova ou excluir a atual. Vamos fazer um modelo para ele:

 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 da tela em si:

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

O layout é bastante simples, a única coisa em que quero me concentrar é na implementação de botões de furto para excluir projetos. No ListView, há o conceito de ContextActions, se você o definir, no iOS elas serão implementadas por furto, no Android - por um longo toque. Essa abordagem é implementada no Xamarin.Forms, porque é nativa para cada plataforma. No entanto, se quisermos deslizar no andróide, precisaremos implementá-lo com as mãos na parte nativa do andróide. Não tenho tarefa de gastar muito tempo nisso, por isso fiquei satisfeito com a abordagem padrão :) Como resultado, o furto no iOS e o menu de contexto no Android são implementados de maneira simples:

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

Substituindo os dados de teste, obtemos a seguinte lista:



Agora vamos para o manipulador de eventos. Vamos começar com um simples - excluir um projeto:

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

Não é bom excluir algo sem perguntar ao usuário, e no Xamarin.Forms é fácil fazer isso usando o método DisplayAlert padrão. Após chamá-lo, a seguinte janela aparecerá:



Esta janela é de iOs. O Android terá sua própria versão de uma janela semelhante.

Em seguida, implementaremos a adição de um novo projeto. Parece que isso é feito por analogia, mas no Xamarin.Forms não há implementação de um diálogo semelhante ao que eu confirmei a exclusão, mas permitindo que você insira texto. Existem duas soluções possíveis:

  • escreva seu próprio serviço que gerará diálogos nativos;
  • implementar algum tipo de solução alternativa no lado do Xamarin.Forms.

Eu não queria perder tempo levantando o diálogo através do nativo, e decidi usar a segunda abordagem, cuja implementação utilizei no tópico: Como criar um simples diálogo InputBox? , ou seja, o método Task InputBox (navegação de navegação).

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

Agora vamos processar toque por linha, para abrir o projeto:

 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 você pode ver no código acima, para acessar a janela do projeto, precisamos do modelo de visualização e do objeto da janela da página.

Gostaria de dizer algumas palavras sobre a navegação. A propriedade Navigation é definida na classe VisualElement e permite que você trabalhe com o painel de navegação em qualquer visualização do seu aplicativo sem precisar rolá-lo para lá com as mãos. No entanto, para que essa abordagem funcione, você ainda precisará criar esse painel. Portanto, no App.xaml.cs, escrevemos:

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

Onde ProjectsPage é exatamente a janela que estou descrevendo agora.

A janela com notas é muito semelhante à janela com projetos, por isso não vou descrevê-la em detalhes, vou focar apenas em nuances interessantes.

O layout desta janela acabou sendo mais complicado, porque cada linha deve exibir mais informações:

Visualização do 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> 


No conteúdo da janela, novamente temos um ListView, anexado à coleção de notas. No entanto, queremos a altura das células no conteúdo, mas não mais do que 150. Para isso, definimos HasUnevenRows = "True" para que o ListView permita que as células ocupem o espaço necessário. Mas nessa situação, as linhas podem solicitar uma altura superior a 150 e o ListView permitirá que sejam exibidas assim. Para evitar isso na célula, usei meu herdeiro no painel Grid: MyCellGrid. Este painel na operação de medição solicita a altura dos elementos internos e a retorna 150 se for maior:

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

Como, de acordo com o TOR, precisamos editar e excluir, além de tocar e deslizar, também o menu que é aberto clicando no botão no canto da linha, adicionando esse botão ao modelo de célula e assinando o toque nele. Nesse caso, se o usuário clicar no botão, ele intercepta o gesto e não receberemos eventos de clique na linha.

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

Com os dados de teste, nosso formulário fica assim:



O processamento das ações do usuário neste formulário é completamente análogo ao que foi gravado para a janela da lista de projetos. Eu quero parar apenas no menu de contexto pelo nosso botão no canto da linha. No começo, pensei em fazê-lo no nível Xamarin.Forms sem problemas.

Na verdade, só precisamos criar uma visão de algo assim:

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

E mostre-o ao lado do botão. No entanto, o problema é que não podemos saber exatamente onde fica “próximo ao botão”. Esse menu de contexto deve estar localizado na parte superior do ListView e, quando aberto, posicionado nas coordenadas da janela. Para fazer isso, você precisa conhecer as coordenadas do botão pressionado em relação à janela. Podemos obter as coordenadas do botão apenas em relação ao ScrollView interno localizado no ListView. Portanto, quando as linhas não são deslocadas, tudo está bem, mas quando as linhas são roladas, devemos levar em consideração a quantidade de rolagem ocorrida ao calcular as coordenadas. O ListView não nos fornece o valor de rolagem. Portanto, ele deve ser retirado do nativo, o que eu realmente não queria fazer. Portanto, decidi seguir o caminho para um caminho mais padrão e simples: mostrar o menu de contexto do sistema padrão. Como resultado, o manipulador de cliques do botão resultará no seguinte:

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

Chamar o método DisplayActionSheet mostra apenas o menu de contexto regular:



Se você perceber, o texto da anotação é exibido no meu controle MyLabel e não no Label comum. Isso é feito para quê. Quando o usuário altera o texto da nota, os fichários são acionados e o novo texto chega automaticamente no Label. No entanto, o Xamarin.Forms não recalcula o tamanho da célula ao mesmo tempo. Os desenvolvedores do Xamarin afirmam que essa é uma operação bastante cara. Sim, e o ListView em si não possui nenhum método que o faça recontar seu tamanho, InvalidateLayout também não ajuda. A única coisa que eles têm para isso é o método ForceUpdateSize do objeto Cell. Portanto, para chegar lá e puxar na hora certa, escrevi meu herdeiro Label e utilizo esse método para cada alteração 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(); } } } 

Agora, depois de editar uma nota, o ListView ajustará automaticamente o tamanho da célula para caber no novo texto.

Ao editar ou criar uma nova nota, uma janela é aberta com o Editor no conteúdo e o botão Salvar na barra de ferramentas:



Essa janela é um pouco diferente da que temos no TK: a falta de um botão redondo na parte inferior. Se você o colocar simplesmente em cima do editor, ele será bloqueado pelo teclado que sai. Ao mesmo tempo, não encontrei uma solução bonita de como movê-lo e não entrei no nativo com uma pesquisa rápida. Portanto, eu o removi e deixei apenas o botão Salvar no painel superior. Esta janela em si é muito simples, portanto omitirei sua descrição.

O que eu quero dizer no final.

O Xamarin.Forms é adequado para aqueles que estão familiarizados com a infraestrutura .NET e trabalham com ela há muito tempo. Eles não precisarão atualizar para novos IDEs e estruturas. Como você pode ver, o código do aplicativo não é muito diferente do código de qualquer outro aplicativo baseado em XAML. Além disso, o Xamarin permite desenvolver e criar aplicativos iOS no Visual Studio para Windows. Ao desenvolver o aplicativo final para testá-lo e construí-lo, você precisará se conectar a uma máquina MacOS. E as bibliotecas podem ser feitas sem ele.

Para começar a escrever aplicativos no Xamarin.Forms, você não precisa de olhos vermelhos no console. Basta instalar o Visual Studio e escrever aplicativos. Tudo o resto já foi resolvido por você. Ao mesmo tempo, não importa como a Microsoft esteja associada a produtos pagos, o Xamarin é gratuito e há versões gratuitas do Visual Studio.

O fato de o Xamarin.Forms usar o .NET Standard nos bastidores fornece acesso a várias bibliotecas já escritas para ele, que facilitarão a vida ao desenvolver seus aplicativos.

O Xamarin.Forms permite adicionar facilmente algo nas partes nativas do seu aplicativo, se você deseja implementar algo específico da plataforma. Lá você obtém o mesmo c #, mas a API é nativa para cada plataforma.

No entanto, é claro, houve algumas deficiências.

A API, disponível na parte geral, é bastante escassa, porque contém apenas o que é comum a todas as plataformas. Por exemplo, como pode ser visto no meu exemplo, todas as plataformas contêm mensagens de alerta e menus de contexto, e isso está disponível no Xamarin.Forms. No entanto, o menu padrão que permite inserir texto está disponível apenas no iOS, portanto, no Xamarin.

Restrições semelhantes são encontradas no uso de componentes. Algo pode ser feito, algo é impossível. O mesmo toque para excluir um projeto ou nota funciona apenas no iOS. No Android, essa ação de contexto será apresentada como um menu que aparece em um toque longo. E se você quiser deslizar no android, bem-vindo à parte do Android e escreva-a com as mãos.

E, claro, desempenho. A velocidade do aplicativo no Xamarin.Forms será, em qualquer caso, menor que a velocidade do aplicativo nativo. Portanto, a própria Microsoft diz que, se você precisar de um aplicativo sem frescuras em termos de requisitos de design e desempenho, o Xamarin.Forms é para você. Se você precisar de beleza ou velocidade, já deve descer para o nativo. Felizmente, o Xamarin também possui versões nativas que já operam imediatamente com suas APIs da plataforma nativa e funcionam mais rápido que os formulários.

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


All Articles