Setembro de 2015

Volume 30 - Número 9

Aplicativos móveis conectados à nuvem: Crie um aplicativo Xamarin com autenticação e suporte offline

Kraig Brockschmidt

Conforme descrito na primeira metade desta série, “Cloud-Connected Mobile Apps - Create a Web Service with Azure Web Apps and WebJobs” (msdn.microsoft.com/magazine/mt185572), publicada na edição de agosto, atualmente muitos aplicativos móveis estão conectados a um ou mais serviços Web que fornecem dados importantes e interessantes. Embora seja fácil fazer chamadas REST API diretas para serviços e respostas de processo no cliente, essa abordagem pode ser dispendiosa em termos de energia da bateria, largura de banda e limitações impostas por diferentes serviços. O desempenho também pode ser prejudicado em hardware com poucos recursos. Senso assim, faz sentido descarregar o trabalho em um back-end personalizado, como demonstramos no projeto "Altostratus", discutido neste artigo.

O back-end do Altostratus, discutido na parte 1, periodicamente coleta e normaliza "conversas" do StackOverflow e do Twitter (apenas para usar duas fontes de dados diferentes) e as armazena em um banco de dados do Microsoft Azure. Isso significa que os dados precisos de que os clientes necessitam podem ser atendidos diretamente pelo back-end, permitindo que dimensionemos o back-end no Azure para acomodar qualquer quantidade de clientes sem atingir os limites dos provedores originais. Ao normalizar os dados no back-end para atender as necessidades do cliente, otimizamos também a troca de dados por meio da API da Web, poupando os sempre escassos recursos de dispositivos móveis.

Neste artigo, discutiremos o aplicativo cliente em detalhes (ver Figura 1). Vamos começar com a arquitetura do aplicativo para definir o contexto geral e, em seguida, tratar do uso do Xamarin e do Xamarin.Forms, da autenticação com o back-end, da criação de um cache offline e da compilação com o Xamarin no Team Foundation Server (TFS) e no Visual Studio Online.

Aplicativo móvel do Xamarin em execução em tablet Android (esq.), Windows Phone (centro) e iPhone (dir.)
Figura 1 Aplicativo móvel do Xamarin em execução em tablet Android (esq.), Windows Phone (centro) e iPhone (dir.)

Arquitetura do aplicativo cliente

O aplicativo cliente tem três páginas ou modos de exibição principais: Configuração, Início e Item, cujas classes compartilham esses nomes (ver Figura 2). Uma página de Logon secundária não tem interface do usuário e é simplesmente o host das páginas da Web do provedor OAuth. Usando uma estrutura básica Model-View-View-Model (MVVM), cada página principal, com exceção do Logon, tem uma classe de modelo de exibição associada para lidar com as relações de vinculação entre os modos de exibição e as classes de modelo de dados que representam itens, categorias e definições de configuração (incluindo uma lista de provedores de autenticação).

Arquitetura do aplicativo cliente Altostratus, mostrando os nomes das classes principais envolvidas (a menos que indicado, os arquivos do projeto correspondem a esses nomes de classe)
Figura 2 Arquitetura do aplicativo cliente Altostratus, mostrando os nomes das classes principais envolvidas (a menos que indicado, os arquivos do projeto correspondem a esses nomes de classe)

Observação: Os desenvolvedores geralmente preferem separar os modos de exibição do XAML em uma biblioteca de classes portátil [PCL] separada dos modelos de exibição e de outras classes, permitindo que os designers trabalhem com modos de exibição de maneira independente, em ferramentas como o Blend. Não fazemos essa separação para simplificar a estrutura do projeto e também porque o Blend não funciona com os controles do Xamarin.Forms neste momento.

O modelo de dados sempre preenche esses objetos a partir de um banco de dados local do SQLite, que é previamente preenchido na primeira execução do aplicativo. A sincronização com o back-end, que acontece no modelo de dados, é um processo simples para recuperar novos dados (só o necessário, para minimizar o tráfego de rede), atualizando o banco de dados com esses dados, limpando todos os dados antigos e instruindo o modelo de dados a atualizar os objetos. Isso dispara uma atualização da interface do usuário, graças à vinculação de dados com os modelos de exibição.

Como discutiremos posteriormente, existem determinados eventos que disparam uma sincronização: apertar o botão de atualização na interface de usuário, alterar a configuração, autenticar com o back-end (que recupera uma configuração salva anteriormente), retomar o aplicativo depois de pelo menos 30 minutos e assim por diante. É claro que a sincronização é a principal comunicação com o back-end por meio da API da Web, mas o back-end também fornece uma API para registrar um usuário, recuperar as configurações de um usuário autenticado e atualizar essas configurações quando um usuário autenticado altera a configuração.

Xamarin.Forms para cliente de plataforma cruzada

Como você já deve ter lido na MSDN Magazine, o Xamarin permite usar o C# e o Microsoft .NET Framework para criar aplicativos para Android, iOS e Windows com uma grande quantidade de código compartilhado entre plataformas. (Para obter uma visão geral de nossa configuração de desenvolvimento, consulte a última seção: “Compilando com Xamarin em TFS e VSO”.) A estrutura Xamarin.Forms aumenta ainda mais essa quantidade, fornecendo uma solução de interface do usuário comum para XAML/C#. Com o Xamarin.Forms, o projeto Altostratus compartilha mais de 95 por cento de seu código em uma única PCL. O único código específico para uma plataforma do projeto são, na verdade, os bits de inicialização que vêm de modelos de projeto, os renderizadores para páginas de logon que lidam com um controle de navegador da Web e algumas linhas de código para copiar o banco de dados do SQLite previamente preenchido no local apropriado para leitura/gravação no repositório local.

Observe que o projeto do Xamarin também pode ser configurado para usar um projeto compartilhado em vez de uma PCL. O Xamarin, no entanto, recomenda o uso de uma PCL, e um dos principais benefícios com o Altostratus é usar a mesma PCL em um aplicativo do console Win32 para criar o banco de dados previamente preenchido. Isso significa que não duplicamos qualquer código de banco de dados para essa finalidade, e o programa inicializador sempre estará em sincronia com o resto do aplicativo.

Lembre-se de que o código compartilhado não reduz muito o esforço necessário para testar detalhadamente o aplicativo em cada plataforma de destino. Essa parte do processo levará o mesmo tempo necessário para escrever cada aplicativo nativamente. Além disso, como o Xamarin.Forms é muito recente, você pode encontrar bugs específicos da plataforma ou outros comportamentos com que precisará lidar em seu código. Para obter mais detalhes sobre os poucos problemas que enfrentamos ao escrever o Altostratus, consulte nossa postagem em bit.ly/1g5EF4j.

Se você tiver problemas estranhos, consulte primeiro o banco de dados de bugs do Xamarin (bugzilla.xamarin.com). Se o seu problema não estiver lá, poste uma pergunta nos fóruns do Xamarin (forums.xamarin.com). A equipe do Xamarin costuma responder prontamente.

Dito isso, lidar com problemas pontuais como esses é muito menos trabalhoso do que conhecer os detalhes de cada camada da interface do usuário da plataforma. E como o Xamarin.Forms é relativamente recente, localizar problemas ajuda a estrutura a se tornar cada vez mais robusta.

Ajustes específicos de plataforma

No Xamarin.Forms, costuma ser necessário fazer ajustes específicos para a plataforma, como afinar o layout. (Consulte o excelente livro de Charles Petzold, “Creating Mobile Apps with Xamarin.Forms” [disponível somente em inglês] [bit.ly/1H8b2q6] para encontrar vários exemplos.) Talvez você também precise lidar com alguns comportamentos inconsistentes, como quando um elemento webview lança o primeiro evento Navigating (mais uma vez, para os poucos problemas que encontramos, consulte nossa postagem em bit.ly/1g5EF4j).

Para isso, o Xamarin.Forms tem uma API Device.OnPlatform < T > (iOS_value, Android_value, Windows_value) e um elemento XAML correspondente. Como você pode imaginar, o OnPlatform retorna um valor diferente de acordo com o tempo de execução atual. Por exemplo, o código XAML a seguir oculta os controles de logon da página de configuração no Windows Phone porque o componente Xamarin.Auth ainda não suporta essa plataforma, por isso sempre executamos um (configuration.xaml): não autenticado.

<StackLayout Orientation="Vertical">
  <StackLayout.IsVisible>
    <OnPlatform x:TypeArguments="x:Boolean" Android="true" iOS="true"
      WinPhone="false" />
  </StackLayout.IsVisible>
  <Label Text="{ Binding AuthenticationMessage }" FontSize="Medium" />
  <Picker x:Name="providerPicker" Title="{ Binding ProviderListLabel }"
    IsVisible="{ Binding ProviderListVisible }" />
  <Button Text="{ Binding LoginButtonLabel}" Clicked="LoginTapped" />
</StackLayout>

Por falar em componentes, o próprio Xamarin é composto principalmente de componentes que abstraem os recursos comuns das plataformas nativas, muitos dos quais são anteriores ao Xamarin.Forms para interface do usuário. Alguns desses componentes são internos do Xamarin, enquanto outros, incluindo as contribuições da comunidade, devem ser adquiridos em components.xamarin.com. Além do Xamarin.Auth, o Altostratus usa o plug-in de conectividade (tinyurl.com/xconplugin) para mostrar um indicador e desabilitar o botão Atualizar quando o dispositivo estiver offline.

Descobrimos que sempre há algum atraso entre quando a conectividade é alterada no dispositivo (o que se reflete na propriedade IsConnected do plug-in) e quando o plug-in aciona o evento. Isso significa que alguns segundos podem se passar até que o dispositivo fique offline e o botão Atualizar seja alterado para o estado desabilitado. Para resolver isso, usamos o evento de comando Atualizar para verificar o status IsConnected do plug-in. Se estiver offline, desabilitamos o botão imediatamente, mas definimos um sinalizador que diz ao manipulador ConnectivityChanged para iniciar uma sincronização automaticamente quando a conectividade for restaurada.

O Altostratus também usa o Xamarin.Auth (tinyurl.com/xamauth) para manipular detalhes de autenticação por meio do OAuth, o que discutiremos em instantes. A preocupação é que, neste momento, o componente só dava suporte a iOS e Android, e não ao Windows Phone, e não estava no escopo do projeto resolver essa deficiência específica. Felizmente, o aplicativo cliente é executado normalmente quando não autenticado, o que significa apenas que as configurações do usuário não são mantidas na nuvem e troca de dados com o back-end não está totalmente otimizada. Quando o componente foi atualizado para oferecer suporte a Windows, só foi preciso apenas remover o rótulo OnPlatform no XAML, como mostrado anteriormente, para que os controles de logon ficassem visíveis.

Autenticação com o back-end

No aplicativo Altostratus como um todo, queríamos demonstrar a mecânica envolvida no armazenamento de algumas preferências específicas do usuário no back-end, de forma de este pudesse aplicar essas preferências automaticamente ao tratar as solicitações HTTP. É claro que, para este aplicativo específico, poderíamos obter o mesmo resultado usando parâmetros URI com as solicitações, mas este exemplo não serve como base para cenários mais complexos. Armazenar as preferências no servidor também permite movê-las entre todos os dispositivos do usuário.

Para trabalhar com qualquer tipo de dados específicos de um usuário, é preciso autenticar este usuário. Não se esqueça de que isso é diferente de conceder uma autorização. A autenticação é uma maneira de identificar um usuário e confirmar que ele é quem diz ser. A autorização, por sua vez, está relacionada às permissões de determinado usuário tem (exemplo: administrador, usuário regular e convidado).

Para a autenticação, usamos logon de redes sociais de terceiros, como Google e Facebook, em vez de implementar nosso próprio sistema de credenciais (e o back-end tem uma API por meio da qual o aplicativo cliente recupera uma lista de provedores para a interface do usuário da página de Configuração). A principal vantagem do logon por meio de redes sociais é não termos que lidar com credenciais, nem com as preocupações de segurança e privacidade dos participantes. O back-end só armazena um endereço de email como nome de usuário, e o cliente gerencia um token de acesso em tempo de execução. Caso contrário, o provedor faria todo o trabalho pesado, incluindo verificação de email, recuperação de senha e assim por diante.

Obviamente, nem todo mundo tem uma conta de provedor de logon social, e alguns usuários não querem usar contas de redes sociais por questões de privacidade. Além disso, o logon em rede social pode não ser apropriado para aplicativos de linha de negócios. Para esses casos, recomendamos o Active Directory do Azure. Para o nosso objetivo, no entanto, é uma escolha lógica, porque só precisamos de alguns meios para autenticar o usuário individual.

Uma vez autenticado, o usuário tem autorização para salvar preferências no back-end. Se quiséssemos implementar outros níveis de autorização (como modificar as preferências de outros usuários), o back-end poderia verificar o nome de usuário contra um banco de dados de permissões.

Usando OAuth2 para logon social na API Web ASP.NET O OAuth2 (bit.ly/1SxC1AM) é uma estrutura de autorização que permite aos usuários conceder acesso a recursos sem compartilhar credenciais. Ele define vários “fluxos de credencial” que especificam como as credenciais são passadas para várias entidades. A API Web ASP.NET usa o chamado “fluxo de concessão implícita”, em que o aplicativo móvel não coleta credenciais nem armazena informações secretas. Este trabalho é feito pelo provedor OAuth2 e pela biblioteca de identidades do ASP.NET (asp.net/identity), respectivamente.

Para habilitar o logon social, você deve registrar seu aplicativo nos provedores de logon usando os portais de desenvolvedor de cada um. (Neste contexto, “aplicativo” significa todas as experiências de cliente, incluindo dispositivos móveis e Web, sem estar vinculada especificamente a um aplicativo móvel). Depois de registrado, o provedor oferece uma ID de cliente exclusiva e um segredo. Consulte bit.ly/1BniZ89 para obter alguns exemplos.

Usamos esses valores para inicializar o middleware de identidade do ASP.NET, conforme mostrado na Figura 3.

Figura 3 Inicializando o middleware de identidade do ASP.NET

var fbOpts = new FacebookAuthenticationOptions
{
  AppId = ConfigurationManager.AppSettings["FB_AppId"],
  AppSecret = ConfigurationManager.AppSettings["FB_AppSecret"]
};
fbOpts.Scope.Add("email");
app.UseFacebookAuthentication(fbOpts);
var googleOptions = new GoogleOAuth2AuthenticationOptions()
{
  ClientId = ConfigurationManager.AppSettings["GoogleClientID"],
  ClientSecret = ConfigurationManager.AppSettings["GoogleClientSecret"]
};
app.UseGoogleAuthentication(googleOptions);

Cadeias de caracteres como "FB_AppID" são chaves que remetem ao ambiente e às definições de configuração do aplicativo Web, onde as IDs e os segredos são armazenados de fato. Isso permite que você os atualize sem recriar e reimplantar o aplicativo.

Usando o Xamarin.Auth para manipular o fluxo de credenciais Em geral, o processo de autenticação social envolve vários handshakes entre o aplicativo, o back-end e o provedor. Felizmente, o componente Xamarin.Auth (disponível no repositório do componente Xamarin) processa a maior parte para o aplicativo. O processo inclui trabalhar com o controle de navegador e fornecer ganchos de retorno de chamada para que o aplicativo possa responder quando a autorização for concluída.

Na configuração inicial, o Xamarin.Auth faz com que o aplicativo cliente execute algumas partes da dança de autorização, o que significa que o aplicativo armazena a ID e o segredo do cliente. Isso é ligeiramente diferente do fluxo do ASP.NET, mas o Xamarin tem uma hierarquia de classes bem construída. A classe do Xamarin.Auth.OAuth2Authenticator deriva do WebRedirectAuthenticator, que fornece a funcionalidade básica necessária e só demanda que escrevamos uma pequena quantidade de código adicional, encontrado nos arquivos LoginPageRenderer.cs nos projetos Android e iOS (porque o Xamarin.Auth ainda não oferece suporte a Windows). Para obter mais detalhes sobre o que fizemos aqui, consulte nossa postagem de blog em tinyurl.com/kboathalto.

Assim, o aplicativo cliente simplesmente tem uma classe de LoginPage que deriva da ContentPage básica do Xamarin.Forms, o que nos permite navegar lá. Essa classe expõe dois métodos, CompleteLoginAsync e CancelAsync, que são chamados do código LoginPageRenderer dependendo do que o usuário faz na interface da Web do provedor.

Envio de solicitações autenticadas Após um logon bem-sucedido, o aplicativo cliente tem um token de acesso. Para fazer uma solicitação autenticada, o app simplesmente inclui esse token em um cabeçalho de autorização como este:

GET http://hostname/api/UserPreferences HTTP/1.1
Authorization: Bearer I6zW8Dk...
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko

Aqui, "Portador" indica autorização mediante o uso de tokens de portador, e depois dele vem a longa e opaca cadeia de caracteres do token.

Usamos a biblioteca System.Net.Http.HttpClient para todas as solicitações REST com um manipulador de mensagem personalizado para adicionar o cabeçalho de autenticação a cada solicitação. Manipuladores de mensagens são componentes de plug-in que permitem examinar e modificar as mensagens de solicitação e resposta HTTP. Para obter mais informações, consulte bit.ly/1MyMMB8.

O manipulador de mensagens é implementado na classe AuthenticationMessageHandler (webapi.cs) e é instalado ao criar a instância do HttpClient:

_httpClient = HttpClientFactory.Create(
  handler, new AuthenticationMessageHandler(provider));

A interface ITokenProvider é apenas uma maneira para o manipulador obter o token de acesso do aplicativo (isso é implementado na classe UserPreferences em model.cs). O método SendAsync é chamado para cada solicitação HTTP. Como mostra a Figura 4, ele adicionará o cabeçalho de Autorização se houver algum disponível no provedor de token.

Figura 4 Adicionando o cabeçalho de Autorização

public interface ITokenProvider
{
  string AccessToken { get; }
}
class AuthenticationMessageHandler : DelegatingHandler
{
  ITokenProvider _provider;
  public AuthenticationMessageHandler(ITokenProvider provider)
  {
     _provider = provider;
  }
  protected override Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
    System.Threading.CancellationToken cancellationToken)
  {
    var token = _provider.AccessToken;
    if (!String.IsNullOrEmpty(token))
    {
      request.Headers.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    }
    return base.SendAsync(request, cancellationToken);
  }
}

No back-end, conforme descrito na parte 1 deste artigo, se um token com uma solicitação é recebido, este é usado para recuperar as preferências do usuário e aplicá-las automaticamente para o restante da solicitação. Se, por exemplo, o usuário definir o limite de conversação para 25 em vez de 100, então o número máximo de itens retornados com uma solicitação será 25, poupando a largura de banda da rede.

Como criar um cache offline para dados de back-end

Uma grande vantagem dos aplicativos móveis em relação à Web para dispositivos móveis é a flexibilidade de suporte a uso offline. Um aplicativo móvel está, por definição, sempre presente no dispositivo do usuário e pode usar uma variedade de opções de armazenamento de dados, como o SQLite, para manter um cache de dados offline, em vez de depender de mecanismos baseados em navegador.

A interface do usuário do cliente móvel Altostratus funciona com dados que são mantidos em um banco de dados local do SQLite, tornando o aplicativo totalmente funcional sem conectividade. Quando há conectividade, os processos em segundo plano recuperam os dados atuais do back-end para atualizar o banco de dados. Isso atualiza os objetos de modelo de dados que ficam acima do banco de dados, que por sua vez dispara atualizações da interface do usuário por meio de associação de dados (consulte a Figura 2). Sendo assim, é uma arquitetura muito semelhante à do back-end discutido na parte 1, em que os trabalhos em andamento coletam, normalizam e armazenam dados em um banco de dados do SQL Server para que a API Web possa atender a solicitações de serviço vindas diretamente desse banco de dados.

O suporte offline para o Altostratus envolve três tarefas distintas:

  • Colocar um arquivo de banco de dados previamente preenchido diretamente no pacote do aplicativo, para fornecer dados de trabalho logo na primeira execução, sem necessidade de conectividade.
  • Implementar processos de sincronização unidirecional para cada parte do modelo de dados que seja exibida na interface do usuário: conversas (itens), categorias, provedores de autenticação e preferências de usuário.
  • Vincular a sincronização aos gatilhos apropriados além do botão Atualizar na interface do usuário.

Vamos examinar cada uma delas detalhadamente.

Criar um banco de dados previamente preenchido É possível que um usuário instale o aplicativo de uma loja online, mas não o execute até um momento posterior, quando o dispositivo estiver offline. A questão é, você quer que o seu aplicativo diga, “Desculpe, não é possível fazer nada útil enquanto você estiver offline”? Ou você deseja que ele lide com esses casos de forma inteligente?

Para demonstrar a segunda abordagem, o cliente Altostratus traz um banco de dados SQLite pré-preenchido direto no pacote do aplicativo em cada plataforma (localizada na pasta recursos/bruto do projeto em cada plataforma). Na primeira execução, o cliente copia o arquivo de banco de dados para um local de leitura/gravação no dispositivo e passa a trabalha com ele exclusivamente a partir desse local. Como o processo de cópia de arquivo é exclusivo para cada plataforma, usamos o recurso Xamarin.Forms.DependencyService para resolver, em tempo de execução, a implementação específica de uma interface que definimos, chamada ISQLite. Isso acontece a partir do construtor DataAccessLayer (DataAccess.cs), que chama ISQLite.GetDatabasePath para recuperar o local específico da plataforma do arquivo de banco de dados de leitura/gravação copiado, conforme mostrado na Figura 5.

Figura 5 Recuperando o local específico da plataforma do arquivo de banco de dados

public DataAccessLayer(SQLiteAsyncConnection db = null)
{
  if (db == null)
  {
    String path = DependencyService.Get<ISQLite>().GetDatabasePath();
    db = new SQLiteAsyncConnection(path);               
    // Alternate use to use the synchronous SQLite API:
    // database = SQLiteConnection(path);               
  }
  database = db;
  _current = this;
}

Para criar o banco de dados inicial, a solução Altostratus contém um pequeno aplicativo de console Win32 chamado DBInitialize. Este aplicativo usa o mesmo PCL compartilhado que o aplicativo para trabalhar com o banco de dados, então não existe risco de ter uma segunda base de código, incompatível. O DBInitialize, no entanto, não precisa usar o DependencyService: Ele pode criar um arquivo diretamente e abrir uma conexão:

string path = "Altostratus.db3";
SQLiteAsyncConnection conn = new SQLiteAsyncConnection(path);
var dbInit = new DataAccessLayer(conn);

A partir daqui, o DBInitialize chama DataAccessLayer.InitAsync para criar as tabelas (algo que o aplicativo nunca tem que fazer com o banco de dados previamente preenchido) e usa outros métodos de DataAccessLayer para obter dados de back-end. Observe que com chamadas assíncronas, o DBInitialize usa apenas usa .Wait, porque é um aplicativo de console e não precisa se preocupar com a interface de usuário dinâmica:

DataModel model = new DataModel(dbInit);
model.InitAsync().Wait();
model.SyncCategories().Wait();
model.SyncAuthProviders().Wait();
model.SyncItems().Wait();

Para conceder aos usuários algo a ser observado, o aplicativo usa um timer para colocar um ponto na tela a cada meio segundo.

Observe que é desejável verificar o banco de dados previamente preenchido com uma ferramenta como o DB Browser for SQLite (bit.ly/1OCkm8Y) antes de puxá-o para o projeto. É possível que uma ou mais das solicitações da Web falhem e, neste caso, o banco de dados não será válido. Você pode compilar essa lógica no DBInitialize para que ele exclua o banco de dados e mostre um erro. Em nosso caso, apenas acompanhamos as mensagens de erro e executamos o programa novamente, quando necessário.

Você deve estar se perguntando: “O conteúdo de um banco de dados previamente preenchido não fica desatualizado relativamente rápido? Eu não quero que os usuários do meu aplicativo vejam dados obsoletos logo na primeira execução!" Isso, é claro, só vai acontecer se você não atualizar o aplicativo regularmente. Portanto, é recomendável se comprometer a fazer atualizações periódicas do aplicativo que incluam um banco de dados com dados razoavelmente atualizados (dependendo da natureza dos seus dados).

Se o usuário já tiver o aplicativo instalado, a atualização não terá qualquer efeito, porque o código que copia o arquivo de banco de dados empacotado verifica se já existe uma cópia de leitura/gravação e a usa. No entanto, se a única verificação for a de existência do arquivo, um usuário que não executar o aplicativo por um tempo poderia acabar iniciando-o com dados mais antigos do que os disponíveis em uma atualização mais recente do pacote. Você pode verificar se o carimbo de data/hora do banco de dados em cache é mais antigo que o do pacote e sobrescrever o cache com a cópia mais recente. Isso, no entanto, não é algo que tenhamos implementado no Altostratus, pois também precisamos preservar as informações de preferências do usuário do banco de dados existente.

Sincronizando o cache offline com o back-end Conforme observado anteriormente, o cliente Altostratus sempre é executado no banco de dados em cache. Todas as solicitações da Web para o back-end (exceto para carregar novas preferências do usuário) ocorrem no contexto de sincronização do cache. O mecanismo para isso é implementado como parte da classe DataModel, especificamente por meio dos quatro métodos sync.cs: SyncSettings (que delega para Configuration.ApplyBack endConfiguration em model.cs), SyncCategories, SyncAuthProviders e SyncItems. Claramente, o trabalho pesado é feito pelo SyncItems, mas falaremos mais sobre o que dispara todos eles na próxima seção.

Observe que o Altostratus sincroniza dados em apenas uma direção, do back-end para o cache local. Além disso, como sabemos que os dados que nos interessam não mudam tão rapidamente (por conta do cronograma dos trabalhos Web do back-end), estamos interessados na consistência com o repositório de dados do back-end, e não em atualizar em minutos ou segundos.

Cada processo de sincronização é essencialmente o mesmo: Recuperar os dados atuais do back-end, atualizar o banco de dados local com novos valores, remover valores antigos do banco de dados e preencher novamente os objetos de modelo de dados para disparar as atualizações na interface do usuário.

Os itens demandam um pouco mais de trabalho porque o SyncItems pode ser invocado a partir da interface do usuário, e queremos nos proteger contra usuários ansiosos que pressionam o botão repetidamente. A propriedade particular DataModel.syncTask indica se há uma sincronização de item ativo, o SyncItems ignora solicitações repetidas se syncTask for não-nulo. Além disso, como uma solicitação de item pode levar algum tempo e envolver conjuntos de dados maiores, queremos que seja possível cancelar uma sincronização de item se o dispositivo ficar offline. Para fazer isso, vamos salvar um System.Threading.CancellationToken para a tarefa.

O método particular SyncItemsCore, mostrado na Figura 6, é o coração do processo. Ele recupera o carimbo de data/hora da última sincronização do banco de dados e o inclui na solicitação da Web.

Figura 6 Método particular SyncItemsCore

private async Task<SyncResult> SyncItemsCore()
{
  SyncResult result = SyncResult.Success;
  HttpResponseMessage response;
  Timestamp t = await DataAccessLayer.Current.GetTimestampAsync();
  String newRequestTimestamp =
    DateTime.UtcNow.ToString(WebAPIConstants.ItemsFeedTimestampFormat);
  response = await WebAPI.GetItems(t, syncToken);
  if (!response.IsSuccessStatusCode)
  {
    return SyncResult.Failed;
  }
  t = new Timestamp() { Stamp = newRequestTimestamp };
  await DataAccessLayer.Current.SetTimestampAsync(t);
  if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
  {
    return SyncResult.NoContent;
  }
  var items = await response.Content.ReadAsAsync<IEnumerable<FeedItem>>();
  await ProcessItems(items);
  // Sync is done, refresh the ListView data source.
  await PopulateGroupedItemsFromDB();
  return result;
}

Ao fazer isso, o back-end retorna somente os itens que são novos ou atualizados desde aquele momento. Como resultado, o cliente obtém somente os dados realmente necessários, economizando o plano de dados potencialmente limitado do usuário. Isso também significa que o cliente trabalha menos para processar os dados de cada solicitação, economizando bateria e minimizando o tráfego de rede. Embora não pareça muito, a manipulação de cinco conversas por categoria, em vez de, digamos, 50, com cada solicitação é uma redução de 90 por cento. Com de 20 a 30 sincronizações por dia, isso poderia facilmente significar centenas de megabytes em um mês para um único aplicativo. Em resumo, os clientes certamente apreciarão os esforços que você fizer para otimizar o tráfego!

Quando a solicitação volta, o método ProcessItems adiciona todos esses itens ao banco de dados, fazendo uma pequena limpeza nos títulos (removendo as aspas inteligentes, por exemplo) e extrai os primeiros 100 caracteres do corpo para mostrar como descrição na lista principal. A limpeza de título é algo que o back-end poderia fazer, economizando um pouco de tempo de processamento no cliente. Preferimos deixar no cliente porque outros cenários talvez exigissem ajustes específicos de plataforma. Também poderíamos fazer com que o back-end criasse as descrições de 100 caracteres, poupando o cliente de um pouco de processamento, mas aumentando o tráfego de rede. Com os dados que trabalhamos aqui, seria provavelmente trocar seis por meia dúzia, mas, como a interface do usuário é, no fim das contas, responsabilidade do cliente, parece melhor deixar que o cliente controle essa etapa. (Para obter mais detalhes sobre isso e mais algumas considerações sobre a interface do usuário, consulte nossa postagem de blog tinyurl.com/kboathaltoxtra.)

Depois de adicionar os itens ao banco de dados, os grupos de modelos de dados são atualizados por meio de PopulateGroupedItemsFromDB. Aqui, é importante entender que o banco de dados provavelmente tem mais itens do que o necessário para o limite atual definido para conversas do usuário. PopulateGroupedItemsFromDB é responsável por isso, aplicando esse limite diretamente à consulta de banco de dados.

Ao longo do tempo, no entanto, não queremos que o banco de dados continue se expandindo por manter vários itens que nunca serão exibidos novamente. Para isso, o SyncItems chama um método DataAccessLayer.ApplyConversationLimit para analisar itens antigos do banco de dados até que o número de itens corresponda a um limite especificado. Como o tamanho de qualquer item individual dentro do conjunto de dados do Altostratus é relativamente pequeno, usamos o limite máximo de 100 conversas, independentemente da configuração atual do usuário. Dessa forma, se o usuário aumentar esse limite, não será preciso solicitar dados do back-end novamente. No entanto, se tivéssemos itens de dados muito maiores, faria mais sentido fazer uma limpeza mais agressiva no banco de dados e solicitar itens novamente, se necessário.

Gatilhos de sincronização O botão Atualizar na interface de usuário é claramente o principal maneira de fazer uma sincronização de item, mas quando os outros processos de sincronização acontecem? Existem outros gatilhos para sincronização de item?

É um pouco complicado responder a essas perguntas em relação ao código, pois todas as chamadas para métodos Sync* ocorrem em um só lugar, o método HomeViewModel.Sync. No entanto, esse método e um ponto de entrada secundário, o HomeViewModel.CheckTimeAndSync, são chamados de vários outros locais. Aqui está um resumo de quando, onde e como as chamadas para Sincronização são parametrizadas com um valor da enumeração SyncExtent:

  • Na inicialização, o construtor HomeViewModel chama Sync(SyncExtent.All) usando um padrão de disparar e esquecer, para que a sincronização ocorra inteiramente em segundo plano. O padrão aqui significa simplesmente salvar o valor de retorno de um método assíncrono em uma variável local para suprimir um aviso do compilador para não usar await.
  • Dentro do manipulador para o evento ConnectivityChanged do plug-in de conectividade, chamamos Sync se o dispositivo estava offline no momento de uma chamada anterior (usando a mesma extensão solicitada então).
  • Se o usuário visitar a página de configuração e fizer alterações nas categorias ativas ou no limite de conversas, ou fizer logon no back-end, com isso, aplicar configurações a partir do back-end, esse fato é lembrado pelo sinalizador DataModel.Configuration.HasChanged. Quando o usuário retornar à página inicial, o manipulador HomePage.OnAppearing chama HomeViewModel.CheckRefresh, que verifica HasChanged e chama Sync(SyncExtent.Items), se necessário.
  • O evento App.OnResume (app.cs) chama CheckTimeAndSync, que aplica uma lógica para determinar o que deve ser sincronizado com base no tempo em que aplicativo esteve suspenso. Claramente, essas condições são altamente dependentes da natureza dos dados e das operações de back-end.
  • Finalmente, o botão Atualizar chama CheckTimeAndSync com um sinalizador para sempre fazer pelo menos uma sincronização de item. O botão Atualizar usa CheckTimeAndSync porque é possível — embora bastante raro — que um usuário tenha deixado o aplicativo em execução em primeiro plano durante mais de meia hora ou até mesmo um dia inteiro, e nesse caso o botão Atualizar também deve fazer outras sincronizações feitas ao retomar.

Um benefício de consolidar tudo em HomeViewModel.Sync é que ele pode definir a propriedade pública HomeViewModel.IsSyncing nos momentos certos. Essa propriedade está associada a dados das propriedades IsVisible e IsRunning de um Xamarin.Forms.ActivityIndicator em Home.xaml. O simples fato de definir esses sinalizador controla a visibilidade daquele indicador.

Compilando com Xamarin no TFS e no Visual Studio Online

Para o projeto Altostratus, usamos um ambiente de desenvolvimento até certo ponto comum para trabalhos multiplataforma: um computador Windows com emuladores e dispositivos amarrados para Android e Windows Phone, juntamente com uma máquina local do Mac OS X com o simulador de iOS e dispositivos amarrados para iOS (ver Figura 7). Com essa configuração, é possível fazer todo o trabalho de desenvolvimento e depuração diretamente no Visual Studio no PC, usando a máquina do Mac OS X para compilação e depuração remota no iOS. Aplicativos iOS prontos para venda também podem ser enviados do Mac.

Um ambiente de desenvolvimento de plataforma cruzada comum para projetos do Xamarin, bem como aqueles que usam outras tecnologias como o Visual Studio Tools for Apache Cordova
Figura 7 Um ambiente de desenvolvimento de plataforma cruzada comum para projetos do Xamarin, bem como aqueles que usam outras tecnologias como o Visual Studio Tools for Apache Cordova

Usamos o Visual Studio Online para colaboração em equipe e controle de origem, configurando o programa para fazer para compilações de integração contínua para o back-end e o cliente Xamarin. Se tivéssemos começamos este projeto hoje, poderíamos ter usado o sistema de compilação mais recente do Visual Studio Online para criar aplicativos Xamarin diretamente no controlador de compilação hospedado. Para saber mais, consulte nossa postagem de blog em tinyurl.com/kboauthxamvso. No início de 2015, no entanto, o controlador de compilação hospedado ainda não tinha esse suporte. Felizmente, é muito fácil — mesmo! — usar um computador local executando o TFS como controlador de compilação para o Visual Studio Online. No servidor, instalamos a edição gratuita do TFS Express com o Xamarin e os SDKs necessários às plataformas Windows e Android, colocando o SDK do Android em um local como c:\android-sdk, que a conta de compilação consegue acessar. (Por padrão, o instalador coloca o SDK no armazenamento do usuário atual, para o qual a conta de compilação não tem permissões.) Isso é abordado na documentação do Xamarin, em “Configuring Team Foundation Server for Xamarin” [disponível somente em inglês], em bit.ly/1OhQPSW.

Quando o servidor de compilação estiver totalmente configurado, as etapas a seguir estabelecem a conexão com o Visual Studio Online (consulte “Implantar e configurar um servidor de compilação” em bit.ly/1RJS4QL):

  1. Abra o Console de administração do TFS.
  2. No painel de navegação à esquerda, expanda o nome do servidor e selecione Configuração da Compilação.
  3. Em serviço de compilação, clique em Propriedades para abrir a caixa de diálogo Propriedades do Serviço de Compilação.
  4. Clique em “Parar o serviço” na parte superior da caixa de diálogo.
  5. Em Comunicações, na caixa abaixo de Fornecer Serviços de Compilação para a Coleção do Projeto, insira a URL de sua coleção do Visual Studio Online, algo como https://<suaconta>.visualstudio.com/defaultcollection.
  6. Clique no botão Iniciar na parte inferior da caixa de diálogo para reiniciar o serviço.

E pronto! Quando você cria uma definição de compilação no Visual Studio Team Explorer, a máquina do TFS conectada ao Visual Studio Online aparecerá na lista de controladores de compilação disponíveis. Ao selecionar esta opção, as compilações que estiverem na fila do Visual Studio ou que são enfileiradas no check-in serão roteadas para o computador do TFS.

Conclusão

Esperamos que você tenha gostado de nossa discussão sobre o projeto Altostratus e que o código seja útil para seus próprios aplicativos móveis e conectados à nuvem. Nosso objetivo com este projeto foi, mais uma vez, fornecer um exemplo claro de um aplicativo móvel de plataforma cruzada com back-end personalizado que pode fazer um trabalho significativo em lugar do cliente, otimizando o trabalho que precisa ser feito diretamente no cliente. Ao fazer o back-end, que está sempre em execução, coletar dados no lugar de todos os clientes, reduzimos consideravelmente a quantidade de tráfego de rede gerado pelo cliente (e o impacto causado por ele nos planos de dados). Ao normalizar os dados de origens diferentes, diminuímos a volume de processamento de dados necessário no cliente, garantindo uma sempre necessária economia de bateria. E ao autenticar um usuário com o back-end, demonstramos como é possível armazenar preferências de usuário e aplicá-las automaticamente às interações do cliente com o back-end, otimizando o tráfego de rede e os requisitos de processamento. Consideramos que, para nossos requisitos específicos, pode haver maneiras mais fáceis de alcançar o mesmo efeito, mas queríamos criar um exemplo dimensionável para cenários mais complexos.

Adoraríamos saber sua opinião sobre esse projeto. Fale conosco!

Sincronização offline do Azure

Uma alternativa à implementação de seu próprio cache offline é a sincronização offline do Azure para tabelas, um dos Serviços Móveis do Azure. Isso elimina a necessidade de escrever códigos de sincronização, e funciona enviando alterações do cliente para o servidor. Por usar armazenamento de tabela, no entanto, a sincronização offline não fornece um modelo de dados relacional, como faz o SQLite.


Kraig Brockschmidt* é desenvolvedor de conteúdo sênior da Microsoft e se concentra em aplicativos móveis de plataforma cruzada. Ele é o autor do livro "Programming Windows Store Apps with HTML, CSS and JavaScript" (duas edições) da Microsoft Press e compartilha outras ideias em kraigbrockschmidt.com.*

Mike Wasson* é desenvolvedor de conteúdo da Microsoft. Por muitos anos, documentou as APIs multimídia do Win32. Atualmente, ele escreve sobre o Microsoft Azure e o ASP.NET.*

Rick Anderson* é escritor técnico de programação sênior da Microsoft, concentrando-se no ASP.NET MVC, no Microsoft Azure e no Entity Framework. Você pode segui-lo no Twitter, em twitter.com/RickAndMSFT.*

Erik Reitan* é um desenvolvedor de conteúdo sênior da Microsoft. Ele se concentra no Microsoft Azure e no ASP.NET. Siga-o no Twitter, em twitter.com/ReitanErik.*

Tom Dykstra* é um desenvolvedor de conteúdo sênior da Microsoft, concentrando-se no Microsoft Azure e no ASP.NET.*

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Michael Collier, Brady Gaster, John de Havilland, Ryan Jones, Vijay Ramakrishnan e Pranav Rastogi