Windows Phone

Crie um aplicativo de golfe multiplataforma usando C# e Xamarin

Wallace B. McClure

Baixar o código de exemplo

Uma das coisas divertidas da volta da temporada de golfe é participar de torneios que incluem eventos como a tacada inicial mais longa. Neles, a primeira jogada de uma pessoa em um determinado buraco é comparada com as de outros participantes do torneio. A mais longa do dia é declarada a vencedora. No entanto, esses concursos tipicamente não têm uma pontuação centralizada. Se você estiver no primeiro grupo, só ficará sabendo ao final do evento como a sua tacada se compara às dos outros. Por que não utilizar um celular para registrar os pontos iniciais e finais de todas as primeiras tacadas e armazenar essa informação em um banco de dados na nuvem?

As opções para criar um aplicativo assim são diversas, o que pode ser causar confusão. Neste artigo, vou mostrar como eu construo um aplicativo assim usando as opções de back-end no Windows Azure e como eu lido com vários problemas. Mostrarei o código para construir o aplicativo tanto para Windows Phone quanto iOS, usando Xamarin.

Vários recursos foram necessários. O aplicativo precisava poder ser executado em diversos dispositivos móveis e sistemas operacionais de celular. Precisava ser um aplicativo nativo, que se parecesse com todos os outros naquele dispositivo. O servidor back-end precisava estar sempre disponível, com o mínimo de esforço para o desenvolvedor (eu). Os serviços de nuvem precisavam fornecer o máximo de ajuda possível na área de desenvolvimento multiplataforma. O banco de dados back-end precisava fornecer alguma funcionalidade de geolocalização.

Por que C#?

Entre as opções para criar aplicativos multiplataforma, temos: aplicativos Web móveis; Xamarin C# para iPhone (e Android); para adotar a multiplataforma e criar aplicativos nas linguagens específicas de cada plataforma (Microsoft .NET Framework para Windows Phone e Objective-C para iPhone); e diversas outras. Primeiramente, a escolha do C#/.NET Framework como linguagem no cliente fez sentido para mim. Como eu tenho um background em C#, isso significa uma coisa a menos para gastar tempo aprendendo depois de aprender os recursos de plataforma específicos de um dispositivo. Ser capaz de desenvolver tudo no Visual Studio 2013 foi um motivo ainda maior para optar por usar a solução do Xamarin para o iPhone. O problema com a opção do aplicativo Web móvel é que os usuários querem aplicativos que se integrem ao máximo possível com sua plataforma. Isso é difícil com essa solução, mas é mais fácil com uma solução nativa. Ignorar o multiplataforma e optar por uma solução específica para cada fornecedor não fez sentido porque exigiria o aprendizado de uma nova linguagem para cada plataforma.

Ferramentas de desenvolvimento

Criar um aplicativo para múltiplas plataformas tipicamente exigia o uso de múltiplas ferramentas de desenvolvimento. Antes do advento do Xamarin.iOS, o desenvolvimento para iPhone só poderia ser feito em um Mac, utilizando o Xamarin Studio (antigamente chamado MonoDevelop, que por sua vez foi portado da ferramenta de código aberto SharpDevelop). Não há nada de errado com o Xamarin Studio, mas os desenvolvedores geralmente preferem ficar o máximo possível dentro da mesma IDE. O uso do Visual Studio 2013 junto ao Xamarin.iOS para Visual Studio permite que se desenvolva para Windows Azure, Windows Phone e iPhone sem a necessidade de se afastar da IDE que você já conhece e ama.

Windows Azure

Um dispositivo móvel pode interagir com o Windows Azure de diversas formas. Entre elas, o Windows Azure Virtual Machine (abreviado como VM daqui para frente), Função Web, Sites do Windows Azure e Windows Azure Mobile Services (WAMS).

Dentre essas opções, uma VM é a que oferece mais controle de todas as variáveis. É possível fazer alterações no seu aplicativo e em todas as configurações do servidor, o que é ótimo para aqueles aplicativos que exigem coisas como a personalização até o nível profundo das configurações do sistema operacional ou a instalação de outro aplicativo, entre outras possíveis alterações. Refere-se a isso como Infraestrutura Como Serviço (IaaS - Infrastructure as a Service).

O Windows Azure tem um tipo de projeto Cloud Service que engloba um conjunto de funções. Funções que tipicamente são mapeadas a projetos no Visual Studio. Uma função Web é basicamente um projeto Web que foi agrupado em um único pacote implantável que foi carregado e é executado dentro de uma VM. Uma função Web oferece interfaces de usuário Web (Web UIs) e também pode conter serviços Web dentro de si. Uma Função de Trabalho é um projeto que será continuamente executado no servidor. Refere-se a isso como Plataforma Como Serviço (PaaS - Platform as a Service).

Os Sites do Windows Azure são conceitualmente similares a uma função Web. Essa solução permitirá a um aplicativo hospedar um site ou projeto. O projeto pode incluir serviços Web. Esses serviços Web podem ser chamados através de um chamado SOAP ou REST. Os Sites do Windows Azure são uma ótima solução para quando um aplicativo só precisa de IIS.

As primeiras três opções exigem que você tenha completo conhecimento de serviços Web — como chamá-los e armazená-los no banco de dados, além de conhecer todo o encanamento que existe entre o dispositivo e a nuvem. A Microsoft tem uma solução que permite armazenar dados na nuvem, lidar com notificações push e autenticar usuários de maneira rápida e fácil: o WAMS. Já tendo criado soluções com outras opções, eu pensei que seria sensato utilizar o WAMS, dada a necessidade de termos a menor quantidade possível de trabalho de encanador, além de bom suporte a multiplataforma. O WAMS às vezes é descrito como “o back-end que você não precisa construir”.

Serviços Móveis do Windows Azure

O WAMS permite acelerar os seus esforços de desenvolvimento para dispositivos móveis, fornecendo armazenamento, autenticação de usuário contra diversas redes sociais, mecanismos para criar lógica no servidor (com Node.js), notificações push e uma biblioteca de código empacotada no lado do cliente para facilitar as operações CRUD (Create, Read, Update, Delete — Criação, Leitura, Atualização e Apagamento) que você terá que fazer nos dados.

O primeiro passo para usar o WAMS é criar um serviço móvel e uma tabela de banco de dados associada. A tabela não é estritamente exigida. Para obter mais detalhes sobre o processo de configuração, consulte o tutorial do Windows Azure em bit.ly/Nc8rWX.

Operações CRUD básicas estão disponíveis no WAMS através de operações de servidor chamados Scripts de servidor. São os arquivos delete.js, insert.js, read.js e update.js, que são processados via Node.js no servidor. Para obter mais informações sobre o Node.js no WAMS, consulte o artigo “Trabalhando com scripts de servidor em Serviços Móveis” no site do Windows Azure em bit.ly/1cHASFA.

Vamos começar dando uma olhada no arquivo insert.js, na Figura 1. Na assinatura do método, o parâmetro “item” contém os dados que serão entregues. Os membros do objeto fazem o mapeamento para o objeto de dados entregue pelo cliente. Os membros desse objeto farão mais sentido após dar uma olhada na seção a respeito de preencher dados a partir do cliente. O parâmetro “user” contém informações a respeito do usuário conectado. Neste exemplo, o usuário precisa ser autenticado. O aplicativo utiliza Facebook e Twitter para autenticação, então o userId que é retornado tem o formato “Rede:12345678”. A parte “Rede” do valor contém o nome do provedor de rede. Neste exemplo, tanto o Facebook quanto o Twitter estão disponíveis, portanto um deles será parte do valor. O número “12345678” é apenas uma representação do userId. Apesar do Twitter e do Facebook estarem sendo usados neste exemplo, o Windows Azure também pode usar uma conta da Microsoft ou do Google.

Figura 1 O arquivo insert.js utilizado quando uma tacada inicial de golfe é inserida na nuvem

function insert(item, user, request) {
  if ((!isNaN(item.StartingLat)) && (!isNaN(item.StartingLon)) &&
    (!isNaN(item.EndingLat)) && (!isNaN(item.EndingLon))) {
    var distance1 = 0.0;
    var distance2 = 0.0;
    var sd = item.StartingTime;
    var ed = item.EndingTime;
    var sdate = new Date(sd);
    var edate = new Date(ed);
    var res = user.userId.split(":");
    var provider = res[0].replace("'", "''");
    var userId = res[1].replace("'", "''");
    var insertStartingDate = sdate.getFullYear() + "-" +
       (sdate.getMonth() + 1) + "-" + sdate.getDate() + " " +
      sdate.getHours() + ":" + sdate.getMinutes() + ":" +
      sdate.getSeconds();
    var insertEndingDate = edate.getFullYear() + "-" +
      (edate.getMonth() + 1) + "-" + edate.getDate() + " " +
      edate.getHours() + ":" + edate.getMinutes() + ":" + 
      edate.getSeconds();
    var lat1 = item.StartingLat;
    var lon1 = item.StartingLon;
    var lat2 = item.EndingLat;
    var lon2 = item.EndingLon;
    var sp = "'POINT(" + item.StartingLon + " " + 
      item.StartingLat + ")'";
    var ep = "'POINT(" + item.EndingLon + " " + 
      item.EndingLat + ")'";
    var sql = "select Max(Distance) as LongDrive from Drive";
    mssql.query(sql, [], {
      success: function (results) {
        if ( results.length == 1)
        {
          distance1 = results[0].LongDrive;
        }
      }
    });
    var sqlDis = "select [dbo].[CalculateDistanceViaLatLon](?, ?, ?, ?)";
    var args = [lat1, lon1, lat2, lon2];
    mssql.query(sqlDis, args, {
      success: function (distance) {
        distance2 = distance[0].Column0;
      }
    });
    var queryString = 
      "INSERT INTO DRIVE (STARTINGPOINT, ENDINGPOINT, " +
      "STARTINGTIME, ENDINGTIME, Provider, UserID, " +
      "deviceType, deviceToken, chanelUri) VALUES " +
      "(geography::STPointFromText(" + sp + ", 4326), " +
      " geography::STPointFromText(" + ep + ", 4326), " +
      " '" + insertStartingDate + "', '" +
      insertEndingDate + "', '" + provider + "', " + userId + ", " +
      item.deviceType + ", '" + item.deviceToken.replace("'", "''") +
       "', " + "'" + item.ChannelUri.replace("'", "''") + "')";
    console.log(queryString);
    mssql.query(queryString, [], {
      success: function () {
        if (distance2 > distance1) {
          if (item.deviceType == 0) {
            push.mpns.sendFlipTile(item.ChannelUri, {
              title: "New long drive leader"
            }, {
                  success: function (pushResponse) {
                    console.log("Sent push:", pushResponse);
                  }
               });
            }
          if (item.deviceType == 1) {
            push.apns.send(item.deviceToken, {
              alert: "New Long Drive",
              payload: {
                inAppMessage: "Hey, there is now a new long drive."
              }
            });
          }
        }
      },
      error: function (err) {
        console.log("Error: " + err);
      }
    });
    request.respond(200, {});
  }
}

A primeira coisa a se fazer é testar o código para validar a entrada. Eu quero verificar que as latitudes e longitudes trazidas são números válidos. Se não forem, o insert vai sair imediatamente. O próximo passo é analisar o userId trazido para obter o fornecedor de rede e o identificador numérico de usuário. O terceiro passo é configurar as datas para que possam ser inseridas no banco de dados. O JavaScript e o SQL Server têm representações de data/hora que não batem, por isso elas devem ser analisadas e colocadas no formato correto.

Agora uma consulta precisa ser feita. Um comando Node.js para realizar uma instrução CRUD chama o mssql.query(comando, parâmetros, callbacks). O parâmetro “comando” é o comando SQL que será executado. O parâmetro “parâmetros” é um array JavaScript que bate com os parâmetros especificados no comando que foi configurado. O parâmetro “callbacks” contém os callbacks JavaScript a serem usados quando a consulta é completada, dependendo de sucesso ou erro. Eu vou explicar o conteúdo de uma consulta inicial bem-sucedida na seção a respeito de notificações push.

Finalmente, surge a questão da depuração. Como você sabe o que está acontecendo no script? O JavaScript tem o método console.log(info). Quando esse método é chamado com o parâmetro “info”, o parâmetro é salvo dentro do arquivo log do serviço, como mostrado na Figura 2. Perceba a funcionalidade de atualização incluída no canto superior direito da tela.

Log File Information in Visual Studio 2013
Figura 2 Informações de arquivo de log no Visual Studio 2013

Uma vez que o WAMS esteja configurado, ele pode ser gerenciado através do portal windowsazure.com ou do Visual Studio.

Observação: Chamados de um arquivo de script do WAMS para um método podem resultar em um erro com a configuração padrão, pois eles são executados em diferentes esquemas. Dependendo da sua situação específica, pode ser necessário garantir permissões. Jeff Sanders tem um post sobre essa questão no seu blog, em bit.ly/1cHQ4Cu.

Escalabilidade

Aplicativos móveis podem colocar uma carga tremenda na infraestrutura, e felizmente o Windows Azure tem diversas opções para cuidar disso. Primeiro, você tem várias alternativas para enfileiramento de mensagens.

O enfileiramento está disponível no Windows Azure através Service Bus, assim como do Windows Azure Queue Service. Ele permite armazenar dados rapidamente, sem prender o aplicativo. Sob tráfego pesado, um aplicativo pode ficar esperando uma responda de uma fonte de dados remota. Em vez de interagir diretamente com uma fonte de dados, um aplicativo pode armazenar dados em uma fila, liberando-o para continuar processando. Falando com base em experiência pessoal, usar enfileiramento pode aumentar fácil e dramaticamente a escalabilidade de um aplicativo. Este aplicativo não usa enfileiramento, mas preciso mencionar que ele é uma opção dependendo da carga de operações e do número de dispositivos móveis acessando o sistema. Felizmente, tanto o Service Bus quanto o Windows Azure Queue Service têm as APIs necessárias para que os scripts de servidor no WAMS acessem cada um deles.

De forma geral, enfileiramento é um ótima solução para aplicativos com tráfego intenso de dados. Outra ferramenta útil para escalabilidade é o recurso de dimensionamento automático. O Windows Azure permite que você monitore a integridade e a disponibilidade de um aplicativo em um painel. É possível configurar regras para notificar um administrador do aplicativo quando a disponibilidade dos serviços estiver prejudicada. O Windows Azure permite que um aplicativo seja dimensionado de acordo com a demanda, para mais ou para menos. Por padrão, esse recurso está desligado. Quando ligado, o Windows Azure verifica periodicamente o número de chamadas da API no serviço e fará o dimensionamento apropriado se ele atingir ou ultrapassar 90% da cota da API. Todos os dias, o Windows Azure é dimensionado de volta ao valor mínimo configurado. A regra geral é configurar a cota diária para servir ao tráfego diário esperado e permitir que o Windows Azure seja dimensionado para mais, se necessário. No momento em que escrevo, os recursos de integridade, monitoramento e dimensionamento automático estão disponíveis na versão preview.

Banco de Dados

Os dados são a raiz de qualquer aplicativo e a base para quase qualquer empresa. Você pode utilizar um banco de dados hospedado de terceiros, um serviço de banco de dados executado em uma VM, o Windows Azure SQL Database e provavelmente diversas outras opções. Para o banco de dados do back-end, eu escolho utilizar o Windows Azure SQL Database em vez de executar o SQL Server dentro de uma VM por diversos motivos. Primeiro, ele tem suporte a serviços baseados em localização no produto base. Segundo, o Windows Azure SQL Database é mais bem-otimizado para desempenho do que uma instalação básica do SQL Server em um sistema cliente. Por último, não existe gerenciamento contínuo do sistema subjacente.

O Windows Azure SQL Database tem os mesmos tipos de banco de dado de pontos e geografia que o SQL Server, então é fácil calcular distâncias entre dois pontos. Para facilitar, eu escrevi dois procedimentos armazenados para calcular essas distâncias. A função SQL Calculate­DistanceViaLatLon recebe os valores de ponto flutuante da latitude e longitude. Ela é projetada para execução dentro do script insert.js do WAMS, então é fácil calcular a distância da tacada que está sendo recebida. O resultado pode ser comparado com a maior tacada atual dentro do sistema. A função SQL CalculateDistance recebe dois pontos de geografia e calcula a distância entre eles, como mostrado na Figura 3. Os dados são armazenados na tabela Drive (tacada) como pontos SQL Server.

Figura 3 Função SQL para calcular distância entre dois pontos

 

    CREATE FUNCTION [dbo].[CalculateDistanceViaLatLon]
    (
      @lat1 float,
      @lon1 float,
      @lat2 float,
      @lon2 float
    )
    RETURNS float
    AS
    BEGIN
      declare @g1 sys.geography = sys.geography::Point(@lat1, @lon1, 4326)
      declare @g2 sys.geography = sys.geography::Point(@lat2, @lon2, 4326)
      RETURN @g1.STDistance(@g2)
    END
    CREATE FUNCTION [dbo].[CalculateDistance]
    (
      @param1 [sys].[geography],
      @param2 [sys].[geography]
    )
    RETURNS INT
    AS
    BEGIN
      RETURN @param1.STDistance(@param2)
    END

A Figura 4 mostra a tabela usada para guardar os dados sobre a tacada. Como as colunas prefixadas com “__” são específicas do Windows Azure, fiz todos os esforços para não utilizá-las. As colunas de interesse são StartingPoint, EndingPoint, Distance, deviceToken e deviceType. As colunas StartingPoint e EndingPoint guardam os pontos geográficos de início e fim de uma tacada. A coluna Distance é uma coluna calculada. Ela é um flutuante que usa a função SQL Calculate­Distance. As colunas deviceToken e deviceType guardam um token que identifica o dispositivo e o tipo de dispositivo (baseado em Windows Phone ou iPhone). O aplicativo atualmente só se comunica de volta com o dispositivo publicador se uma nova unidade que entrou se tornar a nova líder. As colunas deviceToken e deviceType podem ser utilizadas para comunicar que há um novo líder para periodicamente dar outras atualizações aos competidores.

Figura 4 Tabela SQL para guardar dados das tacadas

    CREATE TABLE [MsdnMagGolfLongDrive].[Drive] (
      [id]            NVARCHAR (255)
         CONSTRAINT [DF_Drive_id] 
         DEFAULT (CONVERT([nvarchar](255),newid(),(0))) 
         NOT NULL,
      [__createdAt]   DATETIMEOFFSET (3) CONSTRAINT
        [DF_Drive___createdAt] DEFAULT (CONVERT([datetimeoffset](3),
        sysutcdatetime(),(0))) NOT NULL,
      [__updatedAt]   DATETIMEOFFSET (3) NULL,
      [__version]     ROWVERSION         NOT NULL,
      [UserID]        BIGINT             NULL,
      [StartingPoint] [sys].[geography]  NULL,
      [EndingPoint]   [sys].[geography]  NULL,
      [DateEntered]   DATETIME           NULL,
      [DateUpdated]   DATETIME           NULL,
      [StartingTime]  DATETIME           NULL,
      [EndingTime]    DATETIME           NULL,
      [Distance]      AS                 ([dbo].[CalculateDistance]
        ([StartingPoint],[EndingPoint])),
      [Provider]      NVARCHAR (20)      NULL,
      [deviceToken]   NVARCHAR (100)     NULL,
      [deviceType] INT NULL,
      PRIMARY KEY NONCLUSTERED ([id] ASC)
    );

Esquema dinâmico

Um dos grandes recursos do WAMS é que o esquema de tabelas de banco de dados é dinâmico por padrão. Ele é modificado com base na informação enviada do dispositivo móvel para o cliente. Quando você sair de desenvolvimento e entrar em produção, isso deve ser desligado. A última coisa que você quer é algum tipo de alteração no esquema de um sistema em execução em decorrência de algum erro de programação. Isso pode ser feito facilmente entrando em Configure na seção WAMS do Portal Windows Azure e desligando a opção “esquema dinâmico”.

Acessando dados

Acessar dados em um dispositivo móvel com uma conexão não confiável de alta latência é significativamente diferente de fazer isso através de uma conexão com fios em uma rede de latência relativamente baixa. Há duas regras gerais para obter dados com um dispositivo. Primeiro, o acesso aos dados deve ser feito de maneira assíncrona. Dada a natureza pouco confiável e de alta latência das redes móveis, travar a thread da interface de usuário de qualquer forma é uma ideia muito ruim. Os usuários não entendem por que a interface está lenta. E se a obtenção de dados demora muito, o sistema operacional do dispositivo vai supor que o aplicativo está travado e vai encerrá-lo. A segunda regra é que os dados transferidos devem ser relativamente pequenos. Enviar muitos registros a um dispositivo móvel vai causar problemas em decorrência das redes de baixa velocidade e alta latência comuns em sistemas de provedores móveis e do fato que as CPUs móveis são otimizadas mais para baixo consumo de energia do que para processar dados como uma CPU de desktop ou laptop. O WAMS aborda essas duas questões. O acesso aos dados é assíncrono e as consultas são feitas automaticamente através de um algoritmo de paginação. Para demonstrar isso, eu vou mostrar duas operações, um insert e um select.

Utilizando um Proxy Qualquer desenvolvedor que já fez chamados a serviços baseados em REST sabe sobre o problema de usar REST pelo fato dele não ter pré-construído um serviço de proxy. Isso faz com que trabalhar com REST seja sujeito a erros. Não é impossível — só um pouco mais difícil de trabalhar do que com SOAP. Para facilitar o desenvolvimento, você pode criar um proxy localmente. O proxy para esse exemplo é mostrado na Figura 5. As propriedades de uma instância de objeto podem ser acessadas nos scripts de servidor.

Figura 5 Um proxy para trabalhar com REST

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace Support
{
  public partial class Drive
  {
    public Drive() {
      DeviceToken = String.Empty;
      ChannelUri = String.Empty;
    }
    [JsonProperty(PropertyName="id")]
    public string Id { get; set; }
    [JsonProperty(PropertyName = "UserID")]
    public Int64 UserID { get; set; }
    [JsonProperty(PropertyName = "Provider")]
    public string Provider { get; set; }
    public double StartingLat { get; set; }
    public double StartingLon { get; set; }
    public double EndingLat { get; set; }
    public double EndingLon { get; set; }
    [JsonProperty(PropertyName = "StartingTime")]
    public DateTime StartingTime { get; set; }
    [JsonProperty(PropertyName = "EndingTime")]
    public DateTime EndingTime { get; set; }
    [JsonProperty(PropertyName = "Distance")]
    public double Distance { get; set; }
    [JsonProperty(PropertyName = "deviceType")]
    public int deviceType { get; set; }
    [JsonProperty(PropertyName = "deviceToken")]
    public string DeviceToken { get; set; }
    [JsonProperty(PropertyName = "ChannelUri")]
    public string ChannelUri { get; set; }
  }
}

Consultar dados A consulta a dados no Windows Azure é bem simples, na verdade. Os dados serão retornados por uma chamada através de uma consulta LINQ. Veja aqui um chamado para executar uma consulta simples para retornar os dados: 

var drives = _app.client.GetTable<Support.Drive>();
var query = drives.OrderByDescending(
  drive => drive.Distance).Skip(startingPoint).Take(PageSize);
var listedDrives = await query.ToListAsync();

Nesse exemplo, eu preciso da lista das tacadas iniciais mais longas começando no topo da lista e depois descendo. Isso é, depois, ligado a uma grade tanto em um dispositivo baseado em Windows Phone quanto em iPhone. Apesar da parte de vincular os dados ser diferente, a recuperação deles é exatamente igual.

Perceba que na consulta precedente, não há um chamado a método Where, mas isso poderia ser facilmente feito. Além disso, os métodos Skip e Take são usados para mostrar como o aplicativo poderia facilmente adicionar paginação. A Figura 6 mostra o placar em um dispositivo baseado em Windows Phone e em um iPhone.

The Scoreboard As Depicted on a Windows Phone-Based Device and an iPhone
Figura 6 O placar como exibido em um dispositivo baseado em Windows Phone e em um iPhone

Inserindo dados

Inserir um registro é fácil no WAMS. É só criar uma instância do objeto de dados e chamar método InsertAsync no objeto cliente. O código para a inserção em um dispositivo baseado em Windows Phone é mostrado na Figura 7. O código para executar uma inserção no Xamarin.iOS é parecido, apenas se diferenciando nas áreas do deviceType, ChannelUri e DeviceToken.

Figura 7 Inserindo dados (dispositivo baseado em Windows Phone)

async void PostDrive()
{
  Drive d = new Drive();
  d.StartingLat = first.Latitude;
  d.StartingLon = first.Longitude;
  d.EndingLat = second.Latitude;
  d.EndingLon = second.Longitude;
  d.StartingTime = startingTime;
  d.EndingTime = endingTime;
  d.deviceType = (int)Support.AppConstants.DeviceType.WindowsPhone8;
  d.ChannelUri = _app.CurrentChannel.ChannelUri.ToString();
  try
  {
    await _app.client.GetTable<Support.Drive>().InsertAsync(d);
  }
  catch (System.Exception exc)
  {
    Console.WriteLine(exc.Message);
  }
}

Compartilhando código entre plataformas

Compartilhar código entre plataformas é uma importante consideração e pode ser feito de diferentes modos com as capacidades multiplataforma do C# e do Xamarin. O mecanismo utilizado é determinado pelo cenário exato. Eu precisava compartilhar lógica externa à interface. Há duas opções para fazer isso: Portable Class Libraries (PCL) e arquivos vinculados.

Portable Class Libraries Muitas plataformas usam o .NET Framework. Estas plataformas incluem Windows, Windows Phone, Xbox, Windows Azure e outras suportadas pela Microsoft. Quando o .NET Framework foi inicialmente lançado — e por várias iterações após isso — o código .NET precisava ser recompilado para diferentes plataformas. As PCLs resolvem esse problema. Com um projeto PCL, você configura uma biblioteca para suportar um conjunto definido de APIs disponíveis para as plataformas alvo. Essa escolha de plataformas é definida nas configurações de projeto da biblioteca de classe.

Junto com o suporte a PCLs, há alguns meses a Microsoft alterou seu licenciamento de PCL para permitir o suporte em plataformas que não são dela. Isso permitiu que a Xamarin Inc. oferecesse suporte para os PCLs da Microsoft definidos nas plataformas iOS e Android, assim como no OS X. Documentação sobre o uso de PCLs está disponível.

Arquivos vinculados PCLs são uma ótima solução para desenvolvimento multiplataforma. No entanto, se um recurso for incluído em uma plataforma e não em outra, os arquivos vinculados se tornam a maneira alternativa de compartilhar código. Um arquivo vinculado inclui uma biblioteca básica .NET, as bibliotecas de classe específicas da plataforma e o projeto de aplicativo da plataforma. A biblioteca de classe .NET tem o código geral que é compartilhado entre plataformas.

A biblioteca específica da plataforma contém dois tipos de arquivo. Esses são os arquivos vinculados da biblioteca genérica de classe .NET e o código que teria APIs comuns mas diferentes implementações, específicas por plataforma. A ideia é pegar um arquivo da biblioteca de classes do projeto e “Adicionar como vínculo” à biblioteca de classes específica da plataforma. Isso é mostrado na Figura 8.

Using Linked Files
Figura 8 Usando arquivos vinculados

Outras opções PCLs e arquivos vinculados são apenas duas das opções que você pode usar para compartilhar código. Outras opções incluem classes parciais, opções if/def de compilador, o padrão observador, Xamarin.Mobile e outras bibliotecas disponíveis através do NuGet (ou da Xamarin Component Store), entre outras.

Classes parciais permitem que os arquivos de múltiplas classes sejam compartilhados através da biblioteca de classes compartilhadas e biblioteca de classes específicas da plataforma. Por padrão, os namespaces serão diferentes entre a biblioteca de classe .NET e a biblioteca específica da plataforma. O maior cuidado a ser tomado em relação às classes parciais é que os namespaces precisam corresponder. A não correspondência dos namespaces é um erro comum com classes parciais.

O Visual Studio permite que o código seja compilado dentro ou fora do código através de opções if/then de compilador. Junto com isso, as plataformas podem ser definidas como símbolos de compilação condicional. Eles são configurados nas propriedades do projeto, como mostrado na Figura 9, onde a diretiva #if é usada para compilar condicionalmente o código para Windows Phone.

Defining a Platform as a Conditional Compilation Symbol
Figura 9 Definindo uma plataforma como um símbolo de compilação condicional

O Xamarin.Mobile é um conjunto de bibliotecas com APIs comuns. As bibliotecas estão disponível para Windows Phone, Android e iOS. O Xamarin.Mobile atualmente suporta serviços de localização, contatos e câmera. Eu usei as APIs de geolocalização dele neste aplicativo.

Para determinar localização, o objeto geolocator é um wrapper para o objeto de geolocalização específico da plataforma. Aqui, ele usa a sintaxe ao estilo async do C# 5.0. Quando uma localização é determinada, um chamado é feito no .ContinueWith, e o processamento ocorre:

geo = new Geolocator();
...
await geo.GetPositionAsync(timeout: 30000).ContinueWith(t =>
  {
    first = t.Result;
    LandingSpot.IsEnabled = true;
  }, TaskScheduler.FromCurrentSynchronizationContext());

Observe que os dispositivos costumam fornecer a geolocalização como uma aproximação. Por isso, nem toda distância registrada será perfeitamente precisa.

Quando estão criando aplicativos, os desenvolvedores costumam pensar em camadas lógicas mais altas de um aplicativo chamando níveis inferiores. Por exemplo, um usuário pode tocar em um botão que aciona a detecção de localização. O problema é quando uma camada lógica inferior de um aplicativo precisa chamar uma camada superior. A solução simples é passar uma referência do nível mais alto a algum mais baixo. Infelizmente, isso quase certamente vai impedir que o código de nível inferior seja compartilhado entre plataformas. Isso pode ser superado com eventos. Dispare na camada inferior um evento que é processado na camada superior. Essa é a base do padrão observador.

Vários terceiros criaram bibliotecas que podem ser usadas em diversas plataformas. É possível encontrá-las com o NuGet e a Xamarin Component Store.

Notificações push

Às vezes o aplicativo servidor precisa se comunicar com o dispositivo móvel. Isso pode ser feito pelo WAMS ou pelo Hub de notificações. O WAMS é uma ótima solução para quando você vai enviar um pequeno número de mensagens. O Hub de notificações é projetado para o envio de mensagens para um grande número de dispositivos, por exemplo, “clientes premium” ou “todos os clientes no estado da Califórnia”. Vamos falar aqui da opção do WAMS.

É possível chamar notificações push do WAMS para um dispositivo móvel dentro dos scripts de servidor. O Windows Azure opera muitas das partes complexas das notificações push, mas não consegue abstrair todas as diferenças no modo como as mensagens são enviadas para todas as plataformas diferentes. Felizmente, as diferenças são sutis.

O objeto mpns é usado para enviar mensagens através do Microsoft Push Notification Service (MPNS). Ele contém quatro membros: sendFlipTile, sendTile, sendToast e sendRaw. Cada um deles tem uma assinatura similar. O primeiro parâmetro é o canal que será usado na comunicação. O segundo é um objeto JSON com os parâmetros a serem enviados ao dispositivo. O terceiro parâmetro é um conjunto de callbacks que ocorre em caso de sucesso ou falha da requisição. O código a seguir utilizando o objeto mpns é usado nos scripts de servidor do Windows Azure para enviar uma mensagem caso exista um novo líder no concurso de mais longa tacada inicial:

push.mpns.sendFlipTile(item.ChannelUri, {
  title: "New long drive leader"
}, {
    success: function (pushResponse) {
      console.log("Sent push:", pushResponse);
    }

O resultado é uma atualização do bloco, como mostrado na Figura 10. Perceba na imagem como o bloco foi atualizado para dizer “New long drive leader”.

A Push Message Showing a New Long Drive Leader
Figura 10 Uma mensagem push mostrando um novo líder da tacada inicial mais longa

Usa-se um objeto apns para enviar mensagens para o Apple Push Notification Services (APNS) nos scripts do WAMS. É conceitualmente similar ao objeto mpns. O membro de maior interesse é o método send. Ele tem uma assinatura similar aos métodos send do mpns. A assinatura contém três parâmetros: o deviceToken, que identifica de maneira única um dispositivo; um objeto parâmetro baseado em JSON; e um parâmetro final que é um conjunto de callbacks.

Aqui está o código que mostra como o objeto apns é utilizado para enviar uma mensagem de novo líder a um dispositivo iOS:

push.apns.send(item.deviceToken, {
  alert: "New Long Drive",
  payload: {
    inAppMessage: "Hey, there is now a new long drive."
  }
});

A Figura 11 mostra o código a ser adicionado ao arquivo AppDelegate.cs para lidar com a mensagem enviada ao iPhone. Nesse exemplo, um UIAlertView é exibido para o usuário.

Figura 11 Processando uma mensagem em um iPhone

public override void RegisteredForRemoteNotifications(
  UIApplication application, NSData deviceToken)
{
  string trimmedDeviceToken = deviceToken.Description;
  if (!string.IsNullOrWhiteSpace(trimmedDeviceToken))
  {
    trimmedDeviceToken = trimmedDeviceToken.Trim('<');
    trimmedDeviceToken = trimmedDeviceToken.Trim('>');
  }
  DeviceToken = trimmedDeviceToken;
}
public override void ReceivedRemoteNotification(
  UIApplication application, NSDictionary userInfo)
{
  System.Diagnostics.Debug.WriteLine(userInfo.ToString());
  NSObject inAppMessage;
  bool success = userInfo.TryGetValue(
    new NSString("inAppMessage"), out inAppMessage);
  if (success)
  {
    var alert = new UIAlertView("Got push notification",
      inAppMessage.ToString(), null, "OK", null);
    alert.Show();
  }
}

Se necessário, você pode usar um objeto gcm para enviar mensagens à plataforma Google Cloud Messaging (GCM).

Uma diferença significativa entre as notificações push do Windows e da Apple (e as do Google) é como o sistema do cliente lida com essas mensagens. Uma listagem completa dos sistemas clientes está inclusa no arquivo AppDelegate.cs do projeto Xamarin.iOS do código para download que acompanha este artigo.

E é isso. Boa sorte com seu desenvolvimento de aplicativos e com seu golfe!

Wallace B. McClure é Mestre em engenharia elétrica pelo Georgia Institute of Technology (Georgia Tech). Ele já prestou consultoria e realizou desenvolvimento para empresas pequenas e grandes. McClure já escreveu livros sobre programação para iPhone com o Xamarin.iOS; programação para Android com o Xamarin.Android; arquitetura de aplicativos; ADO.NET e SQL Server; e AJAX. Ele é um Microsoft MVP, ASPInsider, Xamarin MVP e Xamarin Insider, além de ser sócio da Scalable Development Inc. Seus materiais de treinamento para iOS e Android estão disponíveis no Learn Now Online. Seu blog é o morewally.com, e seu Twitter é o twitter.com/wbm.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Kevin Darty (prestador de serviços independente) e Brian Prince (Microsoft)