Tableless

Busca Menu

Criando uma aplicação móvel com Ionic 2 e Angular 2 em dez passos

Seja o primeiro a comentar por

Introdução

À esta altura do campeonato é provável que você já tenha ouvido falar da nova versão deste famoso framework para criação de aplicações móveis híbridas. O Ionic 2 acaba de chegar em seu Release Candidate e, com ele, trás uma série de recursos e otimizações de código, além de um considerável ganho de performance! Muito desse mérito se deve ao Angular (como é chamada a nova versão do framework, que deixa para trás o “JS” ao final do nome) que chega – finalmente – na sua versão estável, provando que não está para brincadeiras.

Depois de passar por várias mudanças e quebras de código à cada novo release, o Ionic 2 agora atinge a maturidade e se torna um competidor ainda mais forte do modelo de desenvolvimento tradicional (nativo). No entanto, se você já está familiarizado com o Ionic 1, a mudança nos conceitos pode lhe soar um tanto quanto desagradáveis à primeira vista. Mas uma vez que você entende como as peças se encaixam, vai perceber que criar aplicações móveis com o framework tornou-se uma atividade ainda mais simples e recompensadora.

O que tem de novo?

O Ionic foi desenvolvido com base no AngularJS, um framework voltado para a criação de aplicações web modernas, construídas com base em uma página HTML5 que atualiza seu conteúdo de maneira dinâmica (as famosas Single Page Applications ou SPAs). Ao tirar proveito dessa arquitetura – e adicionar uma série de estilos que emulam o visual de aplicações nativas – o Ionic facilitou, em muito, a tarefa de construir um app híbrido, ou seja, aquele que executa tanto em smartphones iOS quando Android, otimizando o seu Tempo de Mercado.

Mesmo ainda sendo executado em uma WebView (browser interno dos smartphones), uma aplicação baseada no Ionic 2 é muito mais rápida, modular e escalável, se comparada com a primeira versão. Principalmente porque o framework segue os padrões web mais recentes, como a nova especificação ES6 (ou ES2015), trazendo para o javaScript conceitos como classes, módulos e arrow functions. Além disso, temos também a presença do polêmico TypeScript (opcional), que trás o poder da tipagem para o seu código, com o intuito de minimizar erros, simplificar a injeção de dependências, facilitar testes, e etc.

Too much information - gif

Mas embora tudo isso pareça um verdadeiro balaio de gato que funciona mais como repelente do que atrativo, não se deixe enganar: A versão 2 do Ionic dá um considerável salto de inovação em relação à sua versão original e abre caminho para novas e interessantes tendências que valem a pena serem exploradas!

Mão na massa!

Para entender melhor como se constrói uma aplicação com o Ionic 2, vamos criar uma do zero 😀

A aplicação que iremos construir é um simples leitor de feeds baseado na API do Reddit, o poderoso canal agregador de notícias, onde membros da comunidade podem submeter conteúdos como links, textos, imagens, etc. O app será 100% funcional e poderá ser instalado no seu smartphone e, quem sabe, até mesmo evoluir com a inclusão de novas funcionalidades.

Instalando o framework

Se você já tem o Ionic 1 instalado na sua máquina, basta digitar o seguinte comando no terminal:

npm install -g ionic

Esse comando atualiza o framework para trabalhar com o Ionic 2 sem afetar a instalação da versão 1.

Mas caso você seja marinheiro de primeira viagem, certifique-se que tenha o Node.js instalado na sua máquina e, em seguida, digite no terminal:

npm install -g ionic cordova

Lembre-se de que você também deve ter o SDK do Android e o Java instalados para fazer build para Android e/ou o Xcode para o build no iPhone:

Depois que a instalação for concluída, você pode verificar a versão do framework no terminal, digitando:

ionic -v

Criando um novo projeto

O CLI (Command Line Interface) do Ionic vem com um monte de comandos úteis que nos ajudam na criação e na manutenção dos projetos. Para conferir a lista de comandos disponíveis, digite:

ionic help

Por enquanto o que nos interessa é o comando start. Digite o seguinte no terminal:

ionic start MyReader blank --v2 --appname "Best Reader Ever" --id "com.tableless.myreader"

O comando start oferece três tipos de templates com código boilerplate. São eles:

  • sidemenu – adiciona um menu lateral à aplicação (estilo de navegação padrão no Android);
  • tabs – cria uma navegação baseada em guias (modelo de organização de conteúdo incentivado pelo iOS);
  • blank – cria um projeto com boilerplate básico, sem nenhum template específico.

O comando que digitamos no terminal vai utilizar o template blank. Também passamos mais três parâmetros adicionais: v2 que informa que queremos trabalhar com a versão 2 do Ionic, appname, que define um nome de projeto menos formal e id, que nos possibilita definir o package da aplicação.

Vamos agora acessar a pasta do nosso projeto, digitando:

cd MyReader

Passo 1 – Conhecendo a arquitetura

Depois de tantas configurações e explicações iremos, enfim, para a parte divertida do processo!

Se você visitar a pasta do projeto dentro de src/pages/, vai notar a presença de uma outra pasta chamada home. Dentro dela há três arquivos:

  • home.html;
  • home.scss;
  • home.ts.

Essas pastas e arquivos foram criados como resultado do comando start. O Ionic é baseado no Angular que, por sua vez, considera que os principais componentes de uma aplicação devem ter escopos isolados. Portanto, cada “página” de um projeto tem seu próprio template visual (html), estilo (scss) e classe (ts). Perceba também que, por padrão, o Ionic utiliza Sass para a escrita de CSS e TypeScript para as classes, ao invés de JavaScript puro. Fique à vontade para vasculhar as pastas do projeto e entender como as informações são organizadas, uma vez que este tutorial não tem o propósito de explorar isso com detalhes.

Antes de modificar algo no projeto, vamos verificar o que já foi gerado de graça. Digite no terminal:

ionic serve

Este comando inicia um servidor local na nossa máquina e abre uma nova aba no browser com a aplicação no ar. Como o LiveReload já vem habilitado por padrão, modificações que fizermos no código serão refletidas automaticamente no browser:

Ionic 2 - Screenshot 1 localhost

Vamos ver isso acontecendo em tempo real. Abra o arquivo home.html e remova o código desnecessário até que ele fique assim:

<ion-header>
  <ion-navbar>
    <ion-title>My Feed Reader</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>

</ion-content>

Confira a mudança ocorrendo automaticamente no browser:

Ionic 2 - Screenshot 2 localhost

Agora vamos dar uma olhada no componente responsável por controlar nosso template. Abra o arquivo home.ts:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
   selector: 'page-home',
   templateUrl: 'home.html'
})
export class HomePage {
   constructor(public navCtrl: NavController) {}
}

Perceba que o arquivo é composto por três blocos distintos, que eu chamo carinhosamente de os 3D: Declaration, Decorator e Definition. A primeira parte é onde declaramos componentes externos ou bibliotecas que iremos utilizar em nosso projeto:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

O segundo bloco é composto por um Decorator. No Angular, todo componente tem um “decorador”, que é responsável por fornecer metadados ou informações sobre a classe. No nosso caso, o decorador está dizendo que as modificações no html serão feitas apenas no componente page-home e que este arquivo, ou seja, o template html que iremos utilizar, se chama home.html, veja:

@Component({
   selector: 'page-home',
   templateUrl: 'home.html'
})

Lembre-se que, por padrão, os Decorators ficam sempre em cima do bloco de definição da classe.

O seletor page-home será útil quando for necessário criar regras de estilo em CSS aplicadas apenas à ele.

E, por fim, temos nosso escopo de classe. Classes em qualquer linguagem de programação orientada à objeto servem para definir a estrutura e o comportamento de objetos. Por enquanto o que você precisa saber é que nossa classe tem apenas um construtor que recebe um objeto do tipo NavController por parâmetro. Nosso próximo passo será inserir novos atributos e métodos para definir melhor o seu comportamento:

export class HomePage {
  constructor(public navCtrl: NavController) {}

Passo 2 – Consumindo dados de uma API pública

Agora que você já sabe mais ou menos como as coisas funcionam, vamos fazer rapidamente uma requisição à uma API externa (Reddit) para exibir seu resultado em uma lista no nosso template.

2.1 – Trabalhando com Observables

Inclua o seguinte código em home.ts:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  public feeds: Array<string>;
  private url: string = "https://www.reddit.com/new.json";  

  constructor(public navCtrl: NavController, public http: Http) {

    this.http.get(this.url).map(res => res.json())
      .subscribe(data => {
        this.feeds = data.data.children;
      }); 
  }

}

Caso queira entender melhor sobre os endpoints da API, dê uma olhada nesse link.

O que fizemos acima foi importar o componente Http e injetá-lo no método construtor. Isso nos possibilita acessar sua instância através do objeto this. Note que também estamos importando o operador map da biblioteca rxjs. O rxjs é uma das extensões que compõe a reactiveX (Reactive Extensions), uma biblioteca assíncrona que trabalha com o stream de dados no padrão Observable.

No objeto http estamos fazendo uma requisição do tipo GET à um endpoint que definimos na variável url, acima do método construtor. Note que, com o uso do TypeScript, podemos definir seu escopo (pública ou privada) e ainda definir o seu tipo (string, number, array…). Ponto para o TypeScript!

Em seguida, transformamos o resultado dessa requisição utilizando o operador map e o convertemos para JSON através do método subscribe (“similar” ao método then de uma Promise).

É importante salientar que o map da biblioteca rxjs é utilizado exclusivamente para mapear um array do tipo Observable e não é o mesmo map que utilizamos em um array comum no JavaScript. Aprenda mais sobre requisições remotas com Observables nesse link.

 

Por fim, incluímos o resultado da requisição (agora um objeto do tipo JSON) dentro da variável pública feeds, que aqui representa um array de strings. Seu escopo precisa ser público pois iremos acessar seu conteúdo no template.

2.2 – Exibindo resultado para o usuário

Como você percebeu, dentro do nosso arquivo home.ts há uma referência ao template home.html dentro do bloco @Component. Esse template, na verdade, é aquilo que o usuário realmente vê na tela do seu smartphone, com base no que definimos dentro da nossa classe. Por enquanto ele não está exibindo nada. Modifique o conteúdo de home.html conforme abaixo:

<ion-header>
  <ion-navbar>
    <ion-title>My Feed Reader</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item *ngFor="let feed of feeds">
      {{feed.data.title}}
    </ion-item>
  </ion-list>
</ion-content>

O Ionic fornece uma grande variedade de componentes visuais out of the box que nos permite construir uma interface praticamente idêntica à de uma aplicação nativa. Não só isso como também é capaz de adaptar o seu estilo visual de acordo com cada plataforma (algo que veremos em breve).

A tag <ion-navbar> representa a barra de navegação que fica no topo da tela. Essa barra geralmente comporta o título da aplicação (como visto na tag <ion-title>) mas também pode conter botões de ação e demais itens, caso necessário.

Já as informações dinâmicas sempre são inseridas dentro da tag <ion-content>, como acabamos de fazer ao inserir o componente <ion-list>.

Não iremos nos aprofundar nos detalhes dos templates visuais fornecidos pelo Ionic. Você pode encontrar exemplos do markup de cada componente aqui. O componente que estamos utilizando no exemplo acima é este. Eu apenas copiei o markut e inseri aqui, alterando apenas aquilo que é necessário. Esta é, sem dúvida, uma das features mais importantes do framework, uma vez que ela acelera o processo de prototipação de um aplicativo.

 

Observe o seguinte bloco de código:

  <ion-list>
    <ion-item *ngFor="let feed of feeds">
      {{feed.data.title}}
    </ion-item>
  </ion-list>

Note o loop que estamos executando com a instrução *ngFor. Estamos acessando o conteúdo do array feeds e iterando sobre ele com uma variável local (feed) para popular nossa lista. Esta é uma conveniência fornecida pelo Angular conhecida como Embedded templates ou diretivas html, que nos ajuda na renderização dos atributos disponíveis na classe associada. Observe agora o seguinte trecho:

{{feed.data.title}}

Ele representa o valor que será exibido em cada célula da lista, que, neste caso, representa o título do feed. Esta sintaxe entre chaves duplas é chamada de Interpolação.

Agora salve o arquivo e verifique o resultado no browser. É provável que você esteja vendo algo assim:

Ionic 2 - Screenshot 3 localhost

Se você entendeu tudo que foi explicado até aqui, significa que você já domina boa parte dos principais conceitos não só do Ionic 2 como também do Angular 2. Parabéns!

Agora é o momento em que nos despedimos das explicações mais detalhadas e partimos para a ação. Vamos dar um tapinha no visual desse app e inserir alguns recursos extras que irão torná-lo ainda mais sexy 😉

Passo 3 – Customizando o template

Nosso próximo passo será incluir mais informações nas células dessa lista, uma vez que apenas o título não é o suficiente para capturar a atenção do usuário.

3.1 – Adicionando informações extras

Ainda em home.html, altere o conteúdo atual de dentro da tag <ion-content> para:

 <ion-list>
    <ion-item *ngFor="let feed of feeds">
       <ion-thumbnail item-left>
          <img [src]="feed.data.thumbnail">
       </ion-thumbnail>
       <h2>{{feed.data.title}}</h2>
       <p>{{feed.data.domain}}</p>
    </ion-item>
 </ion-list>

Salve o arquivo e visualize o resultado no browser:

Ionic 2 - Screenshot 4 localhost

Agora estamos utilizando um novo template de lista, que comporta imagens. O Ionic já faz o serviço de ajustar os itens pra você contanto que indiquemos isso através dos atributos e classes que o framework nos oferece. Perceba, por exemplo, o atributo item-left presente dentro da tag <ion-thumbnail>. Altere seu nome para item-right e você verá que as imagens serão posicionadas à direita da célula. Tente também alterar a tag <ion-thumbnail> para <ion-avatar> e verá que as imagens ficarão menores e com bordas arredondadas. Muito conveniente!

Note que o atributo src da tag de imagem está envolto por colchetes. Essa sintaxe se chama Property binding e é utilizada para atribuir uma propriedade da view ao valor de uma expressão. No entanto, a mesma sintaxe pode ser substituída por esta:

<img src="{{ feed.data.thumbnail }}">

Para fins didáticos iremos deixar a expressão com colchetes neste exemplo.

Perceba também que o título do feed agora aparece dentro da tag h2 e um novo item foi inserido dentro de uma tag p. Você pode utilizar o Chrome Developer Tools para inspecionar a conteúdo da listagem disponível no array inserindo a instrução console.log(this.feed); ao fim da requisição, dessa forma:

  this.http.get(this.url).map(res => res.json())
    .subscribe(data => {
      this.feeds = data.data.children;
     // Exibindo conteúdo do array no console do browser
      console.log(this.feeds);
    }); 

Passo 4 – Fornecendo feedback ao cliente e capturando eventos

Embora nossa aplicação consiga requisitar dados externos com sucesso, não há nada que informe ao usuário sobre o status dessa ação. Ele pode aguardar poucos segundos como também pode esperar uma eternidade até que alguma coisa apareça na tela do celular, dependendo do tipo de conexão que esteja enfrentando.

4.1 – Adicionando um Loading

Insira o seguinte conteúdo no arquivo home.ts:

import { Component } from '@angular/core';
import { NavController, LoadingController } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  public feeds: Array<string>;
  private url: string = "https://www.reddit.com/new.json";  

  constructor(public navCtrl: NavController, public http: Http, public loadingCtrl: LoadingController) {

    this.fetchContent();

  }

  fetchContent ():void {
    let loading = this.loadingCtrl.create({
      content: 'Fetching content...'
    });

    loading.present();

    this.http.get(this.url).map(res => res.json())
      .subscribe(data => {
        this.feeds = data.data.children;
        loading.dismiss();
      });  
  }

Salve o arquivo e verifique imediatamente o resultado no browser:

Ionic 2 - Screenshot 5 localhost

O Loading é um ótimo componente para fornecer feedback visual para o usuário, indicando que alguma atividade está sendo executada em background. Nada mais é que uma caixa de diálogo que bloqueia qualquer atividade do usuário até que determinada ação seja concluída. A nossa caixa de diálogo inclui um spinner e um texto indicativo por padrão, mas todas essas opções podem ser customizadas para atender melhor a necessidade do seu app.

Incluir um Loading é extremamente simples: Primeiro nós importamos o componente LoadingController da biblioteca ionic-angular e injetamos o objeto no método construtor. Em seguida, inicializamos o Loading com uma mensagem de feedback e depois apresentamos ele através do método present. Depois nós retiramos o componente da tela caso tenhamos sucesso na requisição através do método dismiss. Simples!

Perceba também que, como boa prática, movemos a requisição da API para um método chamado fetchContent que é então chamado imediatamente no construtor. Outra novidade é a inclusão do tipo de retorno do método, tipado como void. Se você vem de linguagens como Java, sabe que esta é uma maneira de dizer que o método não retorna nada, apenas executa uma ação.

4.2 – Eventos html

Antes de passarmos para a próxima etapa, vamos incluir um evento nas células. Faça a seguinte modificação em home.html:

<ion-item *ngFor="let feed of feeds" (click)="itemSelected(feed)">

Queremos executar alguma ação sempre que o usuário clicar/tocar em uma das células. Conseguimos isso fazendo o binding do método itemSelected no evento html click e passando o feed como argumento. Essa sintaxe de incluir eventos html dentro de parênteses é chamado de Event Binding no Angular.

Agora basta incluir o método dentro da classe:

  itemSelected (feed):void {
    alert(feed.data.url);
  } 

Salve o arquivo e clique em cima de alguma célula. A url do post será exibida em um alert!

Passo 5 – Exibindo o conteúdo de uma url no browser

Agora que você já entendeu como capturar uma ação do usuário, vamos prosseguir com as funcionalidades do nosso app e fazer com que o post seja exibido no browser.

5.1 – Instalando plugin InAppBrowser

Em uma nova aba do terminal, entre na pasta do projeto e digite o seguinte:

ionic plugin add cordova-plugin-inappbrowser

Este plugin nos possibilita abrir sites externos em um browser diretamente do app. Mas só será possível testar essa funcionalidade se você fizer o build para testar no emulador ou no seu próprio dispositivo. Para isso, digite a seguinte instrução no terminal caso você possua um iPhone:

ionic platform add ios

Ou, caso tenha um dispositivo Android:

ionic platform add android

Agora altere o parâmetro do método no arquivo home.html para enviar apenas a url como argumento:

<ion-item *ngFor="let feed of feeds" (click)="itemSelected(feed.data.url)">

E agora basta fazer as seguintes alterações em home.ts. Primeiro, importar a classe do plugin:

import { InAppBrowser } from 'ionic-native';

Em seguida, faça a seguinte alteração no método:

 itemSelected (url: string):void {
   let browser = new InAppBrowser(url, '_system');
 }

Pronto! Agora só resta testar se a funcionalidade está sendo executada conforme desejado.

5.2 – Executando testes nas plataformas

Para instalar o emulador do iOS e preparar o ambiente para testes no seu próprio iPhone, basta digitar no terminal:

npm -g install ios-sim ios-deploy.

Agora digite a instrução abaixo e, caso tudo tenha dado certo, é provável que você veja o aplicativo abrindo no seu emulador:

ionic run ios

Caso esteja com o celular conectado ao computador através da porta USB, o deploy será automaticamente executado no seu iPhone. Se mesmo assim você encontrar dificuldades, tente digitar:

ionic run ios --device

Para testar no Android, apenas digite:

ionic run android

Maiores detalhes sobre deploy e testes em ambas plataformas você encontra aqui.

Caso você tenha conseguido testar com sucesso, deve ter percebido que, ao clicar em uma das células, há um certo delay entre o momento do clique e o carregamento da página. Para corrigir isso, apenas insira o conteúdo da célula dentro de um botão (button) com o atributo ion-item, dessa forma:

 <button ion-item *ngFor="let feed of feeds" (click)="itemSelected(feed.data.url)">
    <ion-thumbnail item-left>
       <img [src]="feed.data.thumbnail">
    </ion-thumbnail>
    <h2>{{feed.data.title}}</h2>
    <p>{{feed.data.domain}}</p>
 </button>

Agora o delay não só é removido como é adicionado um overlay em tom mais escuro na célula quando a mesma é pressionada.

Tem mais uma coisa que está incomodando: Perceba que os posts sem imagens estão quebrando nosso layout e deixando a nossa lista com aspecto pouco profissional. Vamos mudar isso incluindo o seguinte trecho de código dentro do método subscribe de fecthContent:

 this.feeds.forEach((e, i, a) => {
   if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) { 
      e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
   }
 })

Utilizamos o método forEach do JavaScript para iterar pelo array de feeds e verificar quais itens estão sem imagem. Em seguida, para estes itens, incluímos uma imagem padrão do próprio reddit, que está disponível em um link público e irá servir de placeholder.

Veja o resultado de todas estas modificações rodando em um device iOS:

Ionic 2 - InAppBrowser

Passo 6 – Adicionando scroll infinito na célula

Nosso app está ficando bem legal mas ainda necessita de algumas modificações para ficar realmente atrativo. Uma delas é viabilizar alguma maneira de acessar os posts mais antigos, já que nosso app tem uma restrição de apenas 25 itens por request. Isto é muito ruim, pois o usuário fica limitado à visitar apenas estes itens.

Se você explorar a API do reddit vai perceber que ela nos fornece vários parâmetros do tipo GET para controlar filtros e paginações. Um deles é chamado after, que utiliza o o atributo fullName (junção do tipo do post mais o seu ID) como identificador único e funciona como âncora para os demais posts.

Em outras palavras, uma requisição como esta:

https://www.reddit.com/new.json?after=t3_57ct5z

Pode ser lida como: “Busque os novos posts que vem depois do post de nome t3_57ct5z

Fique atento com a forma como você lê a instrução pois há uma pegadinha: Depois aqui se refere ao array de posts, ou seja, os posts mais velhos e não os mais recentes. Veremos como buscar os mais recentes na próxima etapa do app

 

Agora que você já entendeu a mecânica, vamos começar inserindo o componente responsável por acionar o scroll infinito na nossa página. Insira a seguinte instrução em home.html imediatamente após o fim da tag <ion-list>:

 <ion-infinite-scroll (ionInfinite)="doInfinite($event)">
    <ion-infinite-scroll-content
       loadingText="Loading more data...">
    </ion-infinite-scroll-content>
 </ion-infinite-scroll> 

E criamos o método correspondente em nossa classe:

  doInfinite(infiniteScroll) {

    let paramsUrl = (this.feeds.length > 0) ? this.feeds[this.feeds.length - 1].data.name : "";

      this.http.get(this.olderPosts + paramsUrl).map(res => res.json())
        .subscribe(data => {
        
          this.feeds = this.feeds.concat(data.data.children);
          
          this.feeds.forEach((e, i, a) => {
            if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) {  
              e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
            }
          })
          infiniteScroll.complete();
        }); 
  }  

Por fim, inserimos a url da requisição:

private olderPosts: string = "https://www.reddit.com/new.json?after=";

O novo método é bem parecido com o fetchContent, com a diferença de que criamos uma variável local que guarda o valor do atributo nome do último item do array de feeds e insere este valor no fim da url. Em seguida, pegamos o array resultante da requisição e adicionamos no fim do array original através do método concat do JavaScript. Note também que utilizamos o método complete do componente, indicando que o mesmo deve ser removido da view.

O resultado você confere abaixo:

Ionic 2 - Infinite Scroll

E, com isto, incluímos uma funcionalidade extremamente importante em aplicações móveis: A habilidade de adicionar itens em uma lista por demanda, algo que enriquece em muito a experiência do usuário. Se você estava esperando instruções muito complexas, sinto lhe desapontar!

Passo 7 – Atualizando a lista com pull-to-refresh

Da mesma forma que adicionamos uma funcionalidade para carregar posts mais antigos sempre que chegarmos ao fim da nossa lista, precisamos agora viabilizar uma maneira de atualiza-la com os posts mais recentes. Uma excelente maneira de fazer isso é incluindo o componente Refresher na nossa aplicação.

O Refresher é um componente que adiciona o recurso de pull-to-refresh à nossa lista. O pull-to-refresh consiste em manter o dedo pressionado no topo de uma lista e arrastá-la até uma determinada posição até que um evento seja disparado. No nosso caso, utilizaremos esse evento para inserir os posts mais recentes no início do array, ao contrário do que fizemos com o componente InfiniteScroll.

Sem mais delongas, vamos começar inserindo o markup do componente antes da tag <ion-list> no arquivo home.html:

 <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
       pullingIcon="arrow-dropdown"
       pullingText="Pull to refresh"
       refreshingSpinner="circles"
       refreshingText="Refreshing...">
    </ion-refresher-content>
 </ion-refresher> 

Diferentemente do InfiniteScroll, desta vez eu incluí alguns parâmetros adicionais, como os textos de início e fim do evento, o formato padrão do spinner, o ícone da seta, etc.

A url da requisição também precisa ser diferente, uma vez que iremos buscar os itens mais novos. Utilizaremos então o parâmetro before oferecido pelo Reddit, fazendo com que a nossa nova url fique assim:

private newerPosts: string = "https://www.reddit.com/new.json?before=";

Por fim, inserimos o método na classe:

  doRefresh(refresher) {

    let paramsUrl = this.feeds[0].data.name;

    this.http.get(this.newerPosts + paramsUrl).map(res => res.json())
      .subscribe(data => {
      
        this.feeds = data.data.children.concat(this.feeds);
        
        this.feeds.forEach((e, i, a) => {
          if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) {  
            e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
          }
        })
        refresher.complete();
      });
  } 

Perceba como o método é similar àquele que escrevemos para o scroll infinito. A única diferença está na variável de parâmetro (que agora guarda o nome do primeiro item da lista como referência) e a maneira como concatenamos o array de feeds, inserindo os novos dados no início da lista e não no fim. Observe também a instrução refresher.complete, que informa que operação foi concluída e que o componente pode ser removido da view.

Nossa aplicação agora utiliza o refresher para atualizar a lista com os novos posts, veja:

Ionic 2 - Refresher

Passo 8 – Filtrando a lista com uma Action Sheet

Agora que a nossa lista tem potencial para aumentar cada vez mais de tamanho, seria interessante termos uma opção de filtrar posts pertencentes à determinadas categorias. Podemos fazer isso facilmente com uma Action Sheet.

No arquivo home.html vamos incluir um botão do lado direito da nossa AppBar/NavBar que será responsável por disparar o método:

<ion-header>
   <ion-navbar>
       <ion-title>My Feed Reader</ion-title>
       <ion-buttons end>
          <button ion-button icon-only (click)="showFilters()">
              <ion-icon name="funnel"></ion-icon>
          </button>
       </ion-buttons> 
   </ion-navbar>
</ion-header>

Observe o atributo end na tag <ion-buttons>, indicando que o botão deve ser posicionado à direita, ou seja, no fim da barra de navegação. O atributo start posicionaria o botão à esquerda. O <ion-buttons> funciona como um container de botões. No nosso caso, só precisamos de um que será representado por um ícone (por isso o atributo icon-only). Fizemos o binding do método showFilters no evento click e escolhemos o ícone de nome funnel para representar o filtro.

Os ícones no Ionic são uma implementação própria do que chamamos de icon fonts, ou seja, fontes que contém símbolos ao invés de texto ou números e que podem ser estilizados utilizando CSS. Utilizar esse tipo de fonte é conveniente pois reduz a necessidade de imagens, o que torna nosso aplicativo ligeiramente mais rápido e menos pesado. Para ter acesso à lista de ícones do Ionic 2, dê uma olhada aqui.

 

Antes de incluir nosso método, precisamos de mais duas variáveis. Uma que será responsável por guardar a versão íntegra do nosso array de feeds (sem nenhum filtro) e uma outra que será um booleano, com a função de indicar se há ou não um filtro ativo:

 public noFilter: Array<any>;
 public hasFilter: boolean = false;

Com isso podemos incluir as seguintes instruções no final do método subscribe das funções doRefresh e doInfinite, com a finalidade de remover qualquer filtro ativo:

 this.noFilter = this.feeds;
 this.hasFilter = false;

Por fim, vamos agora incluir o método showFilters na classe:

  showFilters() :void {

    let actionSheet = this.actionSheetCtrl.create({
      title: 'Filter options:',
      buttons: [
        {
          text: 'Music',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "music");
            this.hasFilter = true;
          }
        },
        {
          text: 'Movies',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "movies");
            this.hasFilter = true;
          }
        },        
        {
          text: 'Cancel',
          role: 'cancel',
          handler: () => {
            this.feeds = this.noFilter;
            this.hasFilter = false;
          }
        }
      ]
    });

    actionSheet.present();

  }  

Primeiramente inicializamos o componente com a função create em uma variável local. Este componente recebe um título e um array de botões onde cada botão tem, obrigatoriamente, um texto indicativo e um handler que dispara o evento correspondente. Estes botões representam as opções que serão apresentadas para o usuário na tela. O código do filtro é autoexplicativo.

O último botão tem a função de cancelar a operação e remover qualquer filtro que esteja ativo. Perceba que este botão tem uma propriedade role com o valor de cancel, indicando que adota o comportamento padrão da plataforma e sempre estará posicionado como última opção da lista. Vale ressaltar que se o usuário clicar fora da Action Sheet, ou seja, no overlay da camada de fundo, a ação será interpretada como um cancelamento (o mesmo comportamento do botão com a role “cancel”).

Em seguida adicionamos o método actionSheet.present para que o componente seja apresentado na tela.

Por enquanto só estamos filtrando os subreddits com as categorias música ou filmes, mas nada nos impede de inserir mais opções de filtro no componente. O código final da nossa classe fica assim:

import { Component } from '@angular/core';
import { NavController, LoadingController, ActionSheetController } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { InAppBrowser } from 'ionic-native';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  public feeds: Array<any>;
  private url: string = "https://www.reddit.com/new.json";
  private newerPosts: string = "https://www.reddit.com/new.json?before=";  
  private olderPosts: string = "https://www.reddit.com/new.json?after=";

  public hasFilter: boolean = false;
  public noFilter: Array<any>;

  constructor(public navCtrl: NavController, public http: Http, 
       public loadingCtrl: LoadingController, public actionSheetCtrl: ActionSheetController) {

    this.fetchContent();

  }

  fetchContent ():void {
    let loading = this.loadingCtrl.create({
      content: 'Fetching content...'
    });

    loading.present();

    this.http.get(this.url).map(res => res.json())
      .subscribe(data => {
        this.feeds = data.data.children;

        this.feeds.forEach((e, i, a) => {
          if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) {  
            e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
          }
        })

        this.noFilter = this.feeds;  

        loading.dismiss();
      });  
  }

  doRefresh(refresher) {

    let paramsUrl = this.feeds[0].data.name;

    this.http.get(this.newerPosts + paramsUrl).map(res => res.json())
      .subscribe(data => {
      
        this.feeds = data.data.children.concat(this.feeds);
        
        this.feeds.forEach((e, i, a) => {
          if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) {  
            e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
          }
        })

        this.noFilter = this.feeds;
        this.hasFilter = false;

        refresher.complete();
      });
  }  

  doInfinite(infiniteScroll) {

    let paramsUrl = (this.feeds.length > 0) ? this.feeds[this.feeds.length - 1].data.name : "";

      this.http.get(this.olderPosts + paramsUrl).map(res => res.json())
        .subscribe(data => {
        
          this.feeds = this.feeds.concat(data.data.children);
          
          this.feeds.forEach((e, i, a) => {
            if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) {  
              e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
            }
          })

          this.noFilter = this.feeds;
          this.hasFilter = false;          
          
          infiniteScroll.complete();
        }); 
  }   

  itemSelected (url: string):void {
    let browser = new InAppBrowser(url, '_system');
  } 
  
  showFilters() :void {

    let actionSheet = this.actionSheetCtrl.create({
      title: 'Filter options:',
      buttons: [
        {
          text: 'Music',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "music");
            this.hasFilter = true;
          }
        },
        {
          text: 'Movies',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "movies");
            this.hasFilter = true;
          }
        },
        {
          text: 'Games',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "gaming");
            this.hasFilter = true;
          }
        },
        {
          text: 'Pictures',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "pics");
            this.hasFilter = true;
          }
        },                
        {
          text: 'Ask Reddit',
          handler: () => {
            this.feeds = this.noFilter.filter((item) => item.data.subreddit.toLowerCase() === "askreddit");
            this.hasFilter = true;
          }
        },        
        {
          text: 'Cancel',
          role: 'cancel',
          handler: () => {
            this.feeds = this.noFilter;
            this.hasFilter = false;
          }
        }
      ]
    });

    actionSheet.present();

  }        

}

Por questões de bom senso, seria interessante indicar ao usuário quando um filtro está ou não ativo alterando a cor do ícone do funil. Podemos fazer isso utilizando o conceito de Property binding explicado mais acima, com a diferença de que agora a propriedade será atribuída baseada em uma condição.

insira o seguinte código na tag <ion-icon> em home.html:

   <ion-icon name="funnel" [style.color]="hasFilter ? 'orange' : 'inherit'"></ion-icon>

O resultado pode ser visto abaixo:

Ionic 2 - Action Sheet

Passo 9 – Adicionando um provider e uma barra de busca

Apesar de termos avançado com sucesso até aqui, tenho certeza de que a quantidade de código repetitivo presente em nossa classe deve ter te causado um certo incômodo. Podemos muito bem mover a responsabilidade de conexão com a API para um outro serviço externo, no intuito de evitar o DRY e a propagação de code smell.

9.1 – Criando um Injectable

O Angular nos permite criar uma classe com a anotação @Injectable para estes cenários. Esse tipo de classe também são conhecidos como Providers e podem tanto ser criados “na mão” quanto com a ajuda do CLI. Digite no terminal:

ionic g provider RedditService

Esse código cria uma pasta providers no nosso projeto com um arquivo de nome reddit-service.ts, onde o Ionic insere alguns códigos de boilerplate para facilitar nossa vida. Altere seu conteúdo conforme abaixo:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class RedditService {

  private feeds: Array<any>;

  constructor(private http: Http) {}

  fetchData(url: string): Promise<any> {
    
    return new Promise(resolve => {

      this.http.get(url).map(res => res.json())
        .subscribe(data => {
          this.feeds = data.data.children;
          
          this.feeds.forEach((e, i, a) => {
            if (!e.data.thumbnail || e.data.thumbnail.indexOf('b.thumbs.redditmedia.com') === -1 ) {  
              e.data.thumbnail = 'http://www.redditstatic.com/icon.png';
            }
          })
          resolve(this.feeds);
        }, err => console.log(err));          
    });
  }
}

Replicamos boa parte do código presente no método fetchContent da classe home.ts aqui no nosso método fetchData, com algumas diferenças. A primeira delas é a já citada anotação @Injectable() presente antes do nome da classe, o que nos permite mover a definição do serviço para o construtor de home.ts dessa forma:

constructor(public redditService: RedditService) {}

Isso evita que tenhamos de instanciar o serviço utilizando new. Clique aqui para saber mais sobre Injeção de Dependência.

Outra mudança importante é que, por conveniência, a assinatura do método retorna uma Promise do tipo any (para evitar que tenhamos qualquer erro em tempo de compilação) ao invés de um Observable.

Por fim, para utilizar este serviço em nossa classe home.ts precisamos incluí-lo no arquivo app.module.ts, dentro da pasta src/app. Este arquivo faz uso da anotação @NgModule, onde todas as dependências da aplicação devem ser declaradas previamente:

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
//indicamos o source path do arquivo:
import { RedditService } from '../providers/reddit-service';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
//declaramos o nome do nosso provider:
  providers: [RedditService]
})
export class AppModule {}

Com isso é possível escrever os métodos da nossa classe home.ts da seguinte maneira:

 this.redditService.fetchData(this.url).then(data => {
     this.feeds = data;
     this.noFilter = this.feeds;
     loading.dismiss();
 })

Repare que além de muito mais simples, agora utilizamos o método then ao invés do subscribe para recuperar os dados do serviço e preencher nosso array.

9.2 – Adicionando uma SearchBar

Para aplicativos que utilizam listas e exibem conteúdo sob demanda é uma boa prática adicionar algum recurso de busca para que o usuário procure informações com base em uma palavra específica ou sequência de caracteres. Para tal, o Ionic fornece um componente chamado SearchBar.

Para evitar conflitos com as ações da nossa lista, escolhi inserir o componente diretamente na AppBar/NavBar da aplicação. Para tal, insira o seguinte bloco de código dentro da tag <ion-title> em home.html:

 <ion-searchbar 
    [(ngModel)]="searchTerm"
    (ionInput)="filterItems()" 
    placeholder="Type here..." >
 </ion-searchbar>

Perceba que a junção das sintaxes de Event binding e Input binding do ngModel nos permite replicar o tão famoso recurso de Two-way data binding no Angular 2.

Agora inclua o seguinte método em home.ts:

  filterItems() {
    this.hasFilter = false;
    this.feeds = this.noFilter.filter((item) => {
        return item.data.title.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1;
    });
  }

Veja o resultado rodando em um iPhone:

Ionic 2 - SearchBar

Antes de concluir eu gostaria de mostrar um recurso fornecido pelo framework que nos permite testar o comportamento e visual da nossa aplicação em diferentes plataformas chamado Ionic Lab. Caso ainda esteja com o servidor ativo, basta inserir /ionic-lab após o número da porta na url ou digitar ionic serve --lab no terminal. O resultado é o seguinte:

Ionic 2 - Ionic Lab

Na imagem acima você consegue visualizar o nosso aplicativo no iOS, Android e Windows Phone! O Ionic não apenas executa o build da aplicação com uma única base de código para dispositivos diferentes como também se adapta ao comportamento e estilo visual de cada um, numa tentativa de fazer com que o usuário sempre tenha uma experiência condizente com a plataforma que utiliza. Perceba, por exemplo, como o spinner, a barra de busca, os ícones e estilo da lista são diferentes entre as plataformas. No caso do iOS, são incluídos até mesmo as setas na lateral direita da célula, o que é comum na plataforma. Além de tudo isso, poder testar aplicativos dessa maneira e ainda tirar proveito do recurso de LiveReload enquanto você está programando é algo realmente especial.

Estamos chegando ao fim da criação do nosso aplicativo. Vimos que, apenas com pequenos ajustes, foi possível obter um código mais modular e ainda incluir o componente SearchBar com o estilo visual adequado para cada plataforma. Tudo isso de maneira simples e rápida, graças ao casamento perfeito entre o Angular e os componentes estilizados fornecido pelo Ionic.

Passo 10 – Melhorando a experiência do usuário

Mesmo com todos os recursos que o Ionic 2 nos oferece é sempre importante garantir a melhor experiência possível para o usuário fazendo otimizações gerais, como customização de UI, ajustes no comportamento de componentes, ganho de performance, etc. Essa última etapa será dedicada à este propósito.

10.1 – Controlando o scroll

Notei alguns problemas ao utilizar a Action Sheet para filtrar a lista quando o scroll está numa posição muito abaixo, pois a ação de carregar posts antigos pode ser disparada indevidamente. Podemos evitar isso fazendo a lista rolar para o topo antes de executar qualquer filtro. Mas como controlar isso programaticamente?

O componente Content (que gere a tag <ion-content> do nosso template html) disponibiliza um método de controle do scroll chamado scrollToTop. Podemos inserir o código no início do método showFilters da Action Sheet dessa forma:

 this.content.scrollToTop();

Antes precisamos obter uma referência à este componente utilizando a anotação @ViewChild da biblioteca @angular/core (algo similar à maneira como protocolos funcionam no iOS):

 @ViewChild(Content) content: Content;

Agora a lista vai rolar para o topo sempre que acionarmos a Action Sheet!

10.2 – Melhorando a busca com Observables

Apesar de termos nossa barra de buscas funcionando perfeitamente, a cada caractere digitado estamos emitindo uma nova requisição, o que é desnecessário. Mas há uma forma elegante de lidar com isso utilizando Observables, uma vez que o evento só será disparado quando uma requisição for considerada válida.

O que queremos fazer é monitorar o componente de duas maneiras: A primeira é oferecendo um tempo maior para que o usuário conclua a digitação da palavra que está buscando através do método debounceTime e a segunda é utilizando o método distinctUntilChanged que irá comparar a palavra (ou a sequência de caracteres) digitada com a última que foi procurada, evitando que uma nova requisição seja emitida para um resultado que já se encontra na tela.

Iremos utilizar o FormControl de @angular/forms que irá conectar uma variável da classe ao input presente no nosso html (similar à maneira como o Two way binding funciona).

Inclua as seguintes instruções no componente <ion-searchbar> em home.html:

 <ion-searchbar 
    [(ngModel)]="searchTerm"
    [formControl]="searchTermControl" 
    [showCancelButton]=true
    (ionInput)="filterItems()" 
    placeholder="Type here..." >
 </ion-searchbar>

Note que além do formControl também atribuímos o valor true à propriedade showCancelButton, que irá apresentar um botão para cancelar a busca e retirar o teclado digital da tela.

E a seguinte instrução que irá controlar quando devemos disparar a busca:

  this.searchTermControl = new FormControl();
  this.searchTermControl.valueChanges.debounceTime(1000).distinctUntilChanged().subscribe(search => {
    if (search !== '' && search) {
      this.filterItems();
    }
  })  

E com isso o componente se torna mais coerente com a expectativa do usuário, que irá perceber um ganho de performance ao filtrar resultados em uma lista com muitas células.

10.3 – Ajustando o visual dos componentes com CSS

Nosso aplicativo agora depende de algumas mudanças visuais para corrigir alguns pequenos detalhes. O primeiro deles é referente ao Android. O Ionic 2, ao rodar em um dispositivo Android, oferece automaticamente suporte ao Material Design do Google (uma linguagem visual que sintetiza princípios clássicos daquilo que considera o “bom design”). Em resumo, o Material Design se preocupa em criar uma experiência unificada de layout entre as plataformas que rodam o sistema operacional do Android. Aqui você pode conhecer melhor sobre seus princípios fundamentais.

Um dos pontos de atenção é a maneira como os textos devem ser apresentados ao usuário. O Material Design trabalha com a noção de hierarquia baseada em tons e opacidade. Em outras palavras, textos primários (que representam títulos e informações de destaque) recebem 87% de opacidade enquanto subtítulos recebem 54%. Veja abaixo:

Ionic 2 - Material Design Spec

Se você for inspecionar as cores presentes nos textos das células (utilize o Devtools para tal) vai notar que elas não seguem este ponto da especificação da linguagem. O texto principal, por exemplo, utiliza preto puro e é sempre bom (fica aqui a dica) evitar preto puro em seus designs sempre que possível.

Diferentemente do Android, a preocupação do iOS está voltada para o conteúdo, por isso não existe nenhuma linguagem tão restritiva quanto o Material Design na plataforma. No entanto, irei replicar a mudança visual que faremos para o Android também no iOS, tornando nossos textos ainda mais agradáveis para leitura.

Em home.scss inclua o seguinte código:

  // iOS & Android only
  .item-md, .item-ios {
      h2 {
          color: rgba($color: #000, $alpha: .87);
      }
      p {
          color: rgba($color: #000, $alpha: .54);
      }
  }

Repare que podemos fazer o nesting dos elementos por estar utilizando Sass. Também perceba que aplicamos a alteração apenas para as plataformas Android e iOS mas não para Windows Phone. As classes você pode obter facilmente ao inspecionar o DOM no console do browser.

Outro problema aparente são os títulos dos posts que somem ao atingir a borda da célula. Precisamos incluir uma quebra de linha para que eles sejam apresentados por completo. Dessa vez iremos aplicar a alteração às três plataformas:

  // iOS, Android & WP
  .item-md, .item-ios, .item-wp {
      h2, p {
          white-space: normal;
      }
  }

Por fim, gostaria de melhorar a maneira como a barra de busca se apresenta na versão iOS. Ela está pequena e diminui ainda mais de tamanho quando o botão de cancelar está ativo. Também seria interessante escurecer um pouco mais a opacidade do background para lhe conferir maior destaque:

Inclua o seguinte código (desta vez aplicado apenas para o iOS):

  // iOS only
  .toolbar-ios { 
      ion-title {
          padding: 0 90px 0 1px;  
      } 
      .searchbar-ios .searchbar-input {
          background: rgba($color: #000, $alpha: 0.12);            
      }
  }

Caso prefira, você também pode alterar o valor das variáveis Sass do Ionic. Neste link há uma lista de todas elas.

10.4 – Ajustes finais no html

Vamos iniciar modificando a cor da NavBar. Inclua o seguinte atributo na tag <ion-navbar> em home.html:

 <ion-navbar color="secondary">

Como estamos utilizando Sass, fazemos uma referência à variável secondary do array colors que está listado no arquivo src/theme/variable.scss. Isso significa que você pode alterar o valor da cor na variável ao invés de escrever diretamente no template html.

Agora eu gostaria de inserir um ícone na frente do meu endereço de domínio (que representa meu subtítulo na lista) sempre que a categoria estiver relacionada com alguma das listadas na nossa Action Sheet. Eu posso controlar esse comportamento utilizando a diretiva de html do Angular chamada ngSwitch.

Substitua esta linha:

<p>{{feed.data.domain}}</p>

Por esta instrução:

 <div [ngSwitch]=feed.data.subreddit.toLowerCase()>
   <p *ngSwitchCase="'askreddit'"><ion-icon name="help-circle"></ion-icon>&nbsp;{{feed.data.domain}}</p>
   <p *ngSwitchCase="'gaming'"><ion-icon name="logo-playstation"></ion-icon>&nbsp;{{feed.data.domain}}</p>
   <p *ngSwitchCase="'music'"><ion-icon name="musical-notes"></ion-icon>&nbsp;{{feed.data.domain}}</p>
   <p *ngSwitchCase="'movies'"><ion-icon name="film"></ion-icon>&nbsp;{{feed.data.domain}}</p>
   <p *ngSwitchCase="'pics'"><ion-icon name="image"></ion-icon>&nbsp;{{feed.data.domain}}</p>
   <p *ngSwitchDefault>{{feed.data.domain}}</p>
 </div>

Observe que os ícones apenas serão aplicados no caso de coincidirem com os argumentos. Em caso contrário, será exibido apenas o texto sem nenhum ícone, conforme descrito na cláusula ngSwitchDefault.

Outra coisa que me incomoda é o componente Refresher ser acionado com muito pouco esforço. Eu sinto que o usuário poderia puxar um pouco mais a lista para evitar que o evento seja disparado com muita facilidade. Podemos modificar isso alterando a propriedade pullMin, veja:

<ion-refresher (ionRefresh)="doRefresh($event)" [pullMin]=90>

Alteramos para 90 dpi a distância mínima que o usuário deve alcançar para disparar o evento. A distância padrão é 60.

Seria também interessante alterar a cor da barra de status da aplicação para a cor branca, já que o fundo da NavBar agora está colorido. Como a barra de status é um componente nativo, para modificá-lo precisaremos instalar um plugin do Cordova. Verifique se ele já está instalado procurando no arquivo package.json por “cordova-plugin-statusbar“. Caso contrário, digite no terminal:

ionic plugin add cordova-plugin-statusbar

E insira a seguinte instrução dentro do método construtor do arquivo src/app/app.component.ts:

StatusBar.backgroundColorByHexString('#ffffff');

E agora veja como ficou o visual final da nossa aplicação rodando em um iPhone 6:

Ionic 2 - UX enhancement

10.5 – Aumentando o desempenho

Se você pensa em evoluir de um protótipo para um aplicativo real, se preocupar com o seu desempenho é tarefa essencial. Abaixo eu listo algumas sugestões que podem ajudar:

  • WKWebView – Recentemente o time do Ionic tornou possível rodar os aplicativos iOS utilizando o browser WKWebView (evolução do antigo browser UIWebView). Este novo engine oferece aos aplicativos iOS um ganho de performance muito superior ao antigo browser, principalmente na experiência com listas. Para instalar o plugin, digite: ionic plugin add https://github.com/driftyco/cordova-plugin-wkwebview-engine.git --save
  • Crosswalk – Como o Android tem algumas limitações de desempenho que podem ser encontradas em alguns devices (principalmente nos antigos devido às várias versões de sistema existentes), fica difícil garantir que o aplicativo irá rodar exatamente da maneira como queremos. O Crosswalk é um browser moderno que é empacotado junto com o seu app no momento que você faz o build para Android. Isso significa que, independente do device do usuário, ele estará executando o app no Crosswalk. O ganho de performance é visível mas ele pode aumentar o tamanho final da sua aplicação. Para instalar, digite: ionic plugin add cordova-plugin-crosswalk-webview
  • Virtual Scroll – O nosso aplicativo pode adicionar novos itens à lista de várias maneiras. Isso significa que, quanto mais a lista aumenta de tamanho, mais itens precisarão ser renderizados, o que irá consumir muita memória e impactar o desempenho geral. O Virtual Scroll foi criado com o intuito de minimizar este impacto, uma vez que ele apenas renderiza uma quantidade “x” de células na tela, suficientes para preenche-la. Dessa forma elas podem ser reutilizadas, o que evita uma sobrecarga de memória (comportamento muito similar ao de uma ListView no iOS). Para entender melhor sobre como utilizar o Virtual Scroll, visite este link.

Considerações finais

Sim, é um post gigantesco. Mas a minha meta ao escrevê-lo era gerar o máximo de valor para profissionais que ainda não tiveram contato com o Ionic 2 ou aqueles que desejam entender melhor como ele funciona, uma vez que somos carentes de tutoriais mais densos escritos sobre o assunto em português.

Seja você um desenvolvedor, gerente de produto ou CIO de uma empresa, é muito importante compreender que ainda é difícil nos dias de hoje suportar a grande diversidade de aparelhos e plataformas existentes em um ecossistema que vive em constante mudança. Os custos para manter uma equipe multidisciplinar sempre atualizada (e com boa sinergia) é altíssimo e isso se reflete no orçamento repassado para o cliente. Optar pelo desenvolvimento de aplicativos híbridos é, antes de mais nada, uma opção estratégica que deve ser avaliada de acordo com o contexto de cada projeto. Muitas das vezes os argumentos à favor do desenvolvimento nativo não se justificam, principalmente se o projeto não demanda um frame rate muito alto (como aplicativos com muitas animações, transições customizadas ou jogos).

Outro ponto que precisa ser esclarecido é que o Ionic tem um papel importantíssimo no que se refere à uma fase que é tão ou mais importante que o desenvolvimento em si: A prototipação. Poder validar o produto com o cliente ainda em fase inicial é um grande diferencial. Algo que lhe confere uma posição de destaque em um mercado extremamente competitivo.

Conclusão

Há muito espaço para melhorias e recursos adicionais que podem ser implementados no aplicativo que criamos. Caso tope desafios, você pode tentar os seguintes:

  • Incluir data de publicação do post na lista;
  • Incluir recurso de navegação entre telas (deixei o NavController lá de propósito);
  • Oferecer opção de alterar url para exibir resultados de um determinado subreddit;
  • Considerar cenários onde o usuário pode perder a conexão com a internet;
  • Opção de utilizar algum recurso nativo do smartphone (ex: Câmera).

Para facilitar o seu aprendizado, o projeto está disponível no GitHub separado por branches. Por exemplo, caso você queira ter acesso ao passo 4 do tutorial, baixa digitar no terminal git checkout step4 e o código fonte referente à este passo estará disponível.

Bons estudos e até a próxima!

Se você ficou curioso sobre a criação de aplicativos multiplataforma que utilizam tecnologia da web, saiba que o Ionic não é a única opção existente. Leia meu post sobre React Native e descubra como já é possível criar uma aplicação 100% nativa utilizando JavaScript.

Publicado no dia