terça-feira, 5 de maio de 2015

Como consumir webService REST no Android e extrair os dados do fluxo JSON

Estamos de volta! E hoje com um tutorial muito interessante. Vamos aprender como consumir um webService REST a partir do Android e também como extrair os dados provenientes do fluxo JSON.

Pra deixar mais interessante ainda, vamos utilizar como exemplo o serviço "API de GPS dos ônibus",  disponibilizado pela Prefeitura da cidade do Rio de Janeiro em seu portal de dados abertos (Data.rio),  que mostra a posição dos ônibus municipais em tempo real.

Primeiramente é importante salientar que consumir REST no Android é infinitamente mais fácil do que SOAP. REST é simplesmente um serviço disponibilizado na Internet através do protocolo http. É uma URL que é acessada como outra qualquer (usando o método GET). Só que conterá um serviço, recebendo requisições que devolvem dados formatados, a partir dos parâmetros que enviamos.

Geralmente diz-se que é REST um serviço que segue alguns critérios, como por exemplo possuir pontos de entrada que usam substantivos ao final da URL, funcionar sobre o protocolo http, utilizar os códigos de erro http como feedback para o programador, também permitir inserir elementos a partir de um http POST acrescentando dados após o substantivo e obter dados via http GET, geralmente formatados como um fluxo JSON. REST representa uma nova camada conceitual que funciona sobre o protocolo http. Mas, resumidamente, é um serviço disponibilizado em um endereço de Internet onde, passando alguns parâmetros, podemos receber um determinado resultado.

A grande vantagem do REST sobre o SOAP é, como já dissemos, sua trivialidade. Também, possui muito menos overhead, ou seja, o ônus necessário para implementar e consumir REST é infinitamente menor do que SOAP.

Dito isto, vamos ao que interessa.

O serviço REST que utilizaremos neste exemplo pode ser consumido pelo seguinte ponto de entrada:



Clicando no link, veremos que o navegador abre um fluxo de dados contendo as posições dos ônibus da cidade do Rio de Janeiro em tempo real, conforme o exemplo:


{"COLUMNS":["DATAHORA","ORDEM","LINHA","LATITUDE","LONGITUDE","VELOCIDADE","DIRECAO"],
"DATA":[["05-05-2015 00:00:38","B63082","",-22.8678,-43.2584,0.0,""],["05-05-2015 00:00:39","B63058","",-22.7929,-43.2945,0.0,""],["05-05-2015 00:25:45","A48157","",-22.946457,-43.256512,0.0,""],["05-05-2015 00:32:56","B71086","",-22.9031,-43.3137,1.0,""], .... ,["05-05-2015 12:07:23","B32584","",-22.8167,-43.3941,0.0,""]]}


Este fluxo é um JSON ARRAY, ou seja, um vetor de posições de ônibus.  Cada elemento "posição", possui as seguintes informações:

DATAHORA, ORDEM, LINHA, LATITUDE, LONGITUDE, VELOCIDADE, DIRECAO.


O campo "dataHora" mostra o momento em que foi realizada a leitura da posição do ônibus pelo equipamento GPS disponível no interior do veículo. O campo "ordem" mostra a identificação do veículo (é o número impresso na lateral de cada ônibus da cidade do Rio de Janeiro). O campo "linha" é o número da linha de ônibus. Latitude e longitude representam a localização do veículo no momento da leitura pelo GPS. "Velocidade" é a velocidade discreta auferida no momento da leitura. "Direcao" é um valor em graus, em relação ao norte, representando a direção que o veículo tomou desde a leitura anterior do GPS para a última. Este é um campo calculado.

Tipos dos dados:

DataHora é Date, formatado como "MM-dd-yyyy HH:mm:ss".
Ordem é String, pois o ID é composto por uma letra seguida de 5 números.
Linha é String, pois há linhas de ônibus que começam com letras.
Latitude e longitude são Double.
Direção é Integer, variando de 0 a 359 (graus).

O ponto de entrada do serviço REST permite que o consumo seja feito: ou sem nenhum filtro (o endereço sem alterações), ou filtrando pela linha do ônibus (/onibus/[numeroDaLinha]), ou filtrando por um determinado veículo (/onibus/[idVeiculo]). Dessa forma, podemos acompanhar as posições dos veículos de uma linha específica de ônibus ou então de um determinado veículo.

Também é possível informar mais de uma linha de ônibus (ou veículo), acrescentando itens separados por vírgula (por exemplo: /onibus/352,457,474 ou /onibus/c47512,c31497) .

De posse dessas informações, vamos à implementação do consumo.

Primeiro vamos definir a classe "PosicaoOnibusDTO" que descreverá os elementos da nossa lista que armazenará as posições de ônibus advindas da extração dos dados:

package com.example.consumidorrest;

import java.util.Date;

public class PosicaoOnibusDTO
{
   
   private Date dataHora;
   private String ordem;
   private String linha;
   private Double latitude;
   private Double longitude;
   private Double velocidade;
   private Integer direcao;
   
   
   public Date getDataHora()
   {
      return dataHora;
   }
   public void setDataHora(Date dataHora)
   {
      this.dataHora=dataHora;
   }
   public String getOrdem()
   {
      return ordem;
   }
   public void setOrdem(String ordem)
   {
      this.ordem=ordem;
   }
   public String getLinha()
   {
      return linha;
   }
   public void setLinha(String linha)
   {
      this.linha=linha;
   }
   public Double getLatitude()
   {
      return latitude;
   }
   public void setLatitude(Double latitude)
   {
      this.latitude=latitude;
   }
   public Double getLongitude()
   {
      return longitude;
   }
   public void setLongitude(Double longitude)
   {
      this.longitude=longitude;
   }
   public Double getVelocidade()
   {
      return velocidade;
   }
   public void setVelocidade(Double velocidade)
   {
      this.velocidade=velocidade;
   }
   public Integer getDirecao()
   {
      return direcao;
   }
   public void setDirecao(Integer direcao)
   {
      this.direcao=direcao;
   }
   
     
}


Depois, vamos criar o método que consome o serviço, obtendo o JSON, convertendo-o numa String:

// Consome webService REST/JSON via http e retorna uma String com o conteúdo.
String obterPosicoesDeOnibus(String linhaDeOnibus)
{

   // Define variável que conterá o retorno do método.
   String retorno="";
   
   // Define o ENDPOINT (REST/JSON) de consumo.
   String ENDPOINT = "http://dadosabertos.rio.rj.gov.br/apiTransporte/apresentacao/rest/index.cfm/onibus/";
   
   
   // Define o tempo de timeout da conexão e socket, em milissegundos, para a tentativa de obtenção dos dados.
   HttpParams httpParameters=new BasicHttpParams();
   int timeoutConnection=10000;
   HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
   int timeoutSocket=10000;
   HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);

   // Define variável para receber o fluxo de dados proveniente do consumo do webService REST. 
   InputStream inputStream=null;
  
   // Consome o serviço REST, através de uma simples requisição http.
   try
   {
      // Cria cliente http.
      HttpClient httpclient=new DefaultHttpClient(httpParameters);

      // Cria o GET e define a URL
      HttpGet httpget=new HttpGet(ENDPOINT + linhaDeOnibus);

      // Define o tipo de dados para a correta execução do http GET
      httpget.setHeader("Content-Type", "application/json");
      httpget.setHeader("Accept", "application/json");

      // Envia o pedido de consumo da URL através de um GET.
      HttpResponse httpResponse=httpclient.execute(httpget);

      // Recebe a resposta no inputStream.
      inputStream=httpResponse.getEntity().getContent();

      // Define variável para receber o resultado convertido em String.
      String resultado="";

      try
      {
         
         // Converte os dados recebidos no fluxo para String.
         resultado=convertInputStreamToString(inputStream);

         // Verifica se a requisição foi processada corretamente (código http 200).
         // Este serviço envia http status code 200 se foi possível processar a requisição corretamente e há registros.
         // Caso contrário, será enviado um http status code de erro, junto com uma mensagem descritiva no corpo do JSON.
         if (httpResponse.getStatusLine().getStatusCode() == 200)
         {

            // Constrói o retorno do método.
            retorno = resultado;

         }
         else
         {
            // Não foi possivel processar a requisição por alguma razão.
            // Extrai a mensagem de erro enviada.
            try
            {
               JSONObject jObj=new JSONObject(resultado);
               JSONArray dados=null;

               try
               {
                  dados=jObj.getJSONArray("DATA");
                  String msg=dados.get(0).toString().replace("[", "");
                  msg=msg.replace("]", "");
                  msg=msg.replace("\"", "");

                  Log.i("consumidorRest", "Ocorreu um erro ao consumir o serviço: " + msg);

               }
               catch (Exception e)
               {
                  Log.i("consumidorREST", "Erro 1:" + e.getMessage() + " " + e.getLocalizedMessage());
               }
            }
            catch (Exception f)
            {
               Log.i("consumidorREST", "Erro 2:" + f.getMessage() + " " + f.getLocalizedMessage());
            }

         }
      }
      catch (Exception g)
      {
         Log.i("consumidorREST", "Erro 3:" + g.getMessage() + " " + g.getLocalizedMessage());   
      }
   }
   catch (Exception h)
   {
      Log.i("consumidorREST", "Erro 4:" + h.getMessage() + " " + h.getLocalizedMessage());   
   }     

   return retorno;
   
}

Também precisaremos da função convertInputStream, que transforma o resultado em texto:

// Converte a inputString obtida no consumo, para String
private static String convertInputStreamToString(InputStream inputStream) throws IOException
{
   BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream, "ISO-8859-1"));

   int c;
   StringBuilder response=new StringBuilder();

   // Lê do buffer pegando caracter a caracter. Como particularmente este
   // fluxo consumido não possui fim de linha
   // com newline (\n), o método readLine() não funciona.
   while ((c=bufferedReader.read()) != -1)
   {
      response.append((char) c);
   }
   String result=response.toString();
   inputStream.close();

   return result;

}


Com essas duas rotinas podemos consumir o serviço, recebendo o JSON e convertendo-o em texto, para uso posterior. O resultado do consumo será idêntico ao mostrado pelo navegador no caso de um consumo direto, como pode ser visto na imagem abaixo:


JSON consumido via HTTP GET a partir do Android


Mas, para manipularmos essas informações, precisamos extrai-las, armazenando-as em algum tipo de estrutura de dados.

Vamos então criar uma lista de elementos do tipo PosicaoOnibusDTO:

 ArrayList<PosicaoOnibusDTO> posicoesDeOnibus = new ArrayList<PosicaoOnibusDTO>();


Para preencher essa lista, criaremos agora um método que receberá como parâmetro o JSON convertido em texto pelo método anterior e extrairá cada elemento "posição de ônibus", armazenando-o na lista:


public ArrayList<PosicaoOnibusDTO> extrairPosicoesDeOnibus (String jsonString)
{
   // Declara a variável de retorno, que conterá as posições de ônibus.
   ArrayList<PosicaoOnibusDTO> listaDePosicoesDeOnibus = new ArrayList<PosicaoOnibusDTO>();     

   // Declara o DTO que comporá os itens da lista.
   PosicaoOnibusDTO posicaoOnibusDTO;
   
   // Transforma a String em JSONObject.
   JSONObject jObj;
   try
   {
      jObj=new JSONObject(jsonString);
   
      // Extrai do JSONObject os arrays de COLUNAS e POSIÇÕES.
      JSONArray colunas=jObj.getJSONArray("COLUMNS");
      JSONArray posicoesOnibus=jObj.getJSONArray("DATA");

      // Define a variável que guardará uma posição individual de ônibus.
      JSONArray posicaoOnibus;
   
      // Define variáveis para conversão de String em Date.
      String dateString;
      SimpleDateFormat formatter;
      
      String direcao="";
      
      // Itera sobre a String JSON, para extração individual dos dados.
      for (int i=0; i < posicoesOnibus.length(); i++)
      {
         try
         {
            // Extrai uma posição de ônibus individual.
            posicaoOnibus=posicoesOnibus.getJSONArray(i);

            // Instancia o objeto para compor a lista.
            posicaoOnibusDTO = new PosicaoOnibusDTO();
            
            // Faz o parse de cada campo, transferindo para o DTO que comporá a lista.
            dateString = (String) posicaoOnibus.getString(0);
            formatter=new SimpleDateFormat("MM-dd-yyyy HH:mm:ss", Locale.US);
            posicaoOnibusDTO.setDataHora(formatter.parse(dateString)); 
            posicaoOnibusDTO.setOrdem(posicaoOnibus.getString(1));
            posicaoOnibusDTO.setLinha(posicaoOnibus.getString(2).replace(".0", ""));
            posicaoOnibusDTO.setLatitude(Double.valueOf(posicaoOnibus.getString(3)));
            posicaoOnibusDTO.setLongitude(Double.valueOf(posicaoOnibus.getString(4)));
            posicaoOnibusDTO.setVelocidade(Double.parseDouble(posicaoOnibus.getString(5)));
            direcao=((posicaoOnibus.getString(6)).length() == 0) ? "-1" : posicaoOnibus.getString(6);
            direcao=direcao.replace(".0", "");
            posicaoOnibusDTO.setDirecao(Integer.parseInt(direcao));
            
            // Acrescenta o DTO à lista.
            listaDePosicoesDeOnibus.add(posicaoOnibusDTO);
            
         }
         catch (Exception e)
         {
            Log.i("consumidorREST", "Erro 5:" + e.getMessage());
         }
      }
   }
   catch (JSONException f)
   {
      Log.i("consumidorREST", "Erro 6:" + f.getMessage());
   }

   // Retorna a lista de posições de ônibus.
   return listaDePosicoesDeOnibus;
}


Com as informações armazenadas numa estrutura de dados, podemos, por exemplo, jogar ícones representando os ônibus em um mapa. Mas, por ora, vamos apenas mostrar o resultado do consumo no LogCat, para simples visualização e conferência:


// Chama o método para consumo do webService REST/JSON.
// Aqui passamos como parâmetro a linha de ônibus 457. O retorno será uma String com o fluxo JSON que tem
// as posições dos veículos da linha 457, de meia-noite até o momento do consumo.
String linhaDeOnibus = "457";
String jsonString = obterPosicoesDeOnibus(linhaDeOnibus);

// Mostra no LogCat o resultado do consumo.
Log.i("consumidorREST", jsonString);

// Executa o parse do JSON, extraindo as posições de ônibus do fluxo JSON.
ArrayList<PosicaoOnibusDTO> posicoesDeOnibus = new ArrayList<PosicaoOnibusDTO>();
posicoesDeOnibus = extrairPosicoesDeOnibus(jsonString);

if (posicoesDeOnibus.size() > 0)
{
   
   // Variáveis utilizadas na formatação da data.
   String dateString = "";
   SimpleDateFormat formatter=new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.US);
   
   // Itera sobre a estrutura de dados que agora contém as posições dos ônibus.
   for (PosicaoOnibusDTO posicao : posicoesDeOnibus)
   {
      dateString=formatter.format(posicao.getDataHora());
      Log.i("consumidorREST", "Data/Hora  : " + dateString + "");
      Log.i("consumidorREST", "Veículo    : " + posicao.getOrdem() + "");
      Log.i("consumidorREST", "Linha      : " + posicao.getLinha() + "");
      Log.i("consumidorREST", "Latitude   : " + posicao.getLatitude() + "");
      Log.i("consumidorREST", "Longitude  : " + posicao.getLongitude() + "");
      Log.i("consumidorREST", "Velocidade : " + posicao.getVelocidade() + "");
      Log.i("consumidorREST", "Direção    : " + posicao.getDirecao() + "");
      }
   }
}


Após a execução do código, o LogCat mostrará os valores dos campos de cada elemento "posição de ônibus" da lista que criamos a partir da extração dos dados do JSON, como pode ser visto na figura:


Elementos da lista de posições de ônibus



Fica bastante evidente a enorme simplicidade e vantagem de usar REST/JSON em detrimento de SOAP. A única preocupação do desenvolvedor é entender o formato dos dados para fazer a extração.

Para aqueles que preferirem baixar o projeto completo, isto pode ser feito através deste link de download.


E com isto, ficamos por aqui. Espero mais uma vez que tenha sido proveitoso. Caso haja alguma dúvida ou sugestão, não hesite em entrar em contato. Responderei aos comentários tão logo seja possível.

Em nosso próximo tutorial, aprenderemos como utilizar o GPS do dispositivo móvel do usuário para determinar sua localização.

Um grande abraço e até lá!

8 comentários:

  1. Muito bom o tutorial, e como faria isso numa plataforma como essa do Rio que todos os dados em JSON, XML fossem atualizados em tempo real?

    ResponderExcluir
    Respostas
    1. O exemplo do tutorial consome os dados publicados no Data.rio, portal de dados abertos da Cidade do Rio de Janeiro. Esses dados, das posições dos ônibus, são atualizados em tempo real. Para nosso algoritmo, não faz diferença. Cada vez que ele consumir, trará as posições atualizadas. E obrigado pelo comentário!

      Excluir
  2. Este comentário foi removido pelo autor.

    ResponderExcluir
  3. Pergunto pelo fato que preciso desenvolver um web service com ajax por exemplo que atualize minhas informações em tempo real no portal de dados abertos de Natal/RN.

    ResponderExcluir
  4. Então... Geralmente se faz assim: Criamos um webService que disponibiliza as informações (que podem ser em tempo real ou estáticas). Esse webService reside em um backend (um servidor de APIs). Aí então podemos criar toda sorte de consumidores desse serviço: APPs, Páginas web comuns, programas em Java, etc. Aqui no Rio, fizemos um servidor de APIs com inúmeros webServices, que são dispostos para busca através do Data.rio, que foi feito em CKAN.

    ResponderExcluir
  5. Alexsandra, eu tenho uma ferramenta openSource que desenvolvi que pode ajudá-la. Se quiser conhecer, entre em contato comigo.

    ResponderExcluir
  6. Olá Luiz, com o android 6.0 as classes "HttpClient" e "HttpParams" estão em desuso, qual é a nova maneira de conectar?

    obrigado pelo tutorial, abraço!

    ResponderExcluir
  7. Peteer, basta fazer uma requisição web (http) usando GET. Seja qual for o novo método, o protocolo permanece o mesmo. Mas mesmo que estejam "deprecated", você pode contuinuar usando, elas ainda funcionam.

    ResponderExcluir