Esta é a segunda parte do tutorial de como consumir APIs em um app Android usando Retrofit e ButterKnife. Caso tenha caído aqui de pára-quedas, sugiro ler (e programar) a primeira parte do tutorial primeiro, seja clicando no link anterior ou usando o sumário logo abaixo (os itens 1 a 3 são da parte 1 do tutorial).
Apenas recapitulando, estou usando uma API escrita em Node.js com banco MySQL, cujo tutorial e fontes se encontram neste post. Esta API e seu banco estão hospedados na Umbler, que fornece MySQL gratuito para pequenos projetos.
Sobre as bibliotecas-foco desse tutorial, a Retrofit permite abstrair requisições HTTP a APIs usando classes Java de uma maneira muito elegante e produtiva. Enquanto o Butter Knife permite fazer o binding de recursos do seu app (componentes visuais, imagens, cores, strings, eventos, etc) com muito menos código do que normalmente é necessário.
E pra encerrar a introdução, a belíssima tela (#sqn) do nosso app está como abaixo:
Dito isso, veremos neste tutorial completo os seguintes itens (do 1 ao 3 já foram vistos na parte 1):
Vamos lá!
#4 – Usando Retrofit
Como mencionado anteriormente, Retrofit é um HTTPClient que agiliza bastante algumas tarefas tediosas de mapear APIs HTTP em objetos Java para tornar as requisições mais type-safe.
Neste tutorial, temos uma API REST de clientes que possui algumas operações elementares que precisamos mapear para métodos e classes Java, o que o Retrofit vai nos permitir fazer muito facilmente. Mas antes de sair usando ele, precisamos adicioná-lo como uma dependência em nosso build.gradle (no Module: app), na seção dependencies (tal qual fizemos com o ButterKnife):
1 2 3 4 |
compile 'com.squareup.retrofit2:retrofit:2.3.0' compile 'com.squareup.retrofit2:converter-gson:2.3.0' |
Aqui, eu adicionei o Retrofit e o converter que vai nos permitir serializar e desserializar JSON usando a biblioteca Gson (isso porque minha API trabalha com JSON, isso pode variar no seu caso). Apenas atente que o converter utilizado deve ser da mesma versão do seu Retrofit (2.3.0 no meu caso).
E no arquivo proguard-rules.pro, onde tem as configurações do Proguard, adicione as seguintes linhas (recomendação do dev do projeto Retrofit):
1 2 3 4 5 6 7 8 9 10 |
# Platform calls Class.forName on types which do not exist on Android to determine platform. -dontnote retrofit2.Platform # Platform used when running on Java 8 VMs. Will not be used at runtime. -dontwarn retrofit2.Platform$Java8 # Retain generic type information for use by reflection by converters and adapters. -keepattributes Signature # Retain declared checked exceptions for use by a Proxy instance. -keepattributes Exceptions |
Atenção: se você não sabe o que é o Proguard, ele é um otimizador, minificador, ofuscador e pré-verificador de código Java que roda automaticamente segundo uma série de regras para tornar o APK final o menor possível (no caso do Android) e o mais “seguro” possível, do ponto de vista de ofuscação de código. Mais informações nesta resposta do Quora.
Com isso temos o Retrofit configurado em nosso projeto e pronto para usar. O primeiro passo é adicionar uma classe Java que representa um cliente do seu banco de dados, o que eu fiz abaixo do jeito mais simples possível:
1 2 3 4 5 6 7 8 9 10 |
public class Cliente { int ID; String Nome; String CPF; public Cliente(){ } } |
Atenção: foi usada aqui a mesma capitalização (maiúsculas e minúsculas) das colunas da tabela Clientes do meu banco de dados MySQL. Ajuste de acordo com o seu cenário. Caso os nomes não coincidam, a desserialização não acontecerá corretamente mais tarde e os métodos retornarão clientes “vazios”.
Agora, adicione uma interface Java ao nosso projeto, que representará a API localmente. Aqui eu chamei de ClientesService.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public interface ClientesService { @GET("clientes/") Call<List<Cliente>> selectClientes(); @GET("clientes/{id}") Call<List<Cliente>> selectCliente(@Path("id") int id); @FormUrlEncoded @POST("clientes") Call<Cliente> insertCliente(@Field("nome") String nome, @Field("cpf") String cpf); @FormUrlEncoded @PATCH("clientes/{id}") Call<Cliente> updateCliente(@Path("id") int id, @Field("nome") String nome, @Field("cpf") String cpf); @DELETE("clientes/{id}") Call<Cliente> deleteCliente(@Path("id") int id); } |
Para cada endpoint da nossa API (falei sobre isso na parte 1 deste tutorial, lembra?), criamos um método na interface usando annotations para os verbos HTTP e para os parâmetros que devam vir no path e no body da requisição HTTP.
Para quem entende um mínimo de web APIs, o código é auto-explicativo.
Atenção: meu método que retorna cliente por id está retornando uma lista de clientes propositalmente porque na minha API Node+MySQL eu sempre retorno um array JSON em GETs. Outro ponto de atenção são os métodos de insert e update que possuem a annotation @FormUrlEncoded pois minha API Node+MySQL espera chave-valor no body (caso esperasse JSON, eu usaria um @Body Cliente como parâmetro). Métodos com esta annotation devem possuir parâmetros com @Field indicando o nome de cada chave que será enviada no body. Para saber os fields corretos, consulte a documentação da API (no meu caso, consulte o tutorial original).
Agora, para podermos usar esta interface, vamos adicionar uma nova variável e um método de carregamento dessa variável em nossa MainActivity.java:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static ClientesService api = null; private void carregarAPI(){ Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://10.0.2.2:3000/") .addConverterFactory(GsonConverterFactory.create()) .build(); MainActivity.api = retrofit.create(ClientesService.class); } |
Esse método de carregamento deve ter sua baseUrl apontada para a sua URL (no meu caso, node.luiztools.com.br). Caso queira apontar para uma webapi rodando localhost, use 10.0.2.2 como URL (e inclua a porta, se necessário). Além disso, esse método usa a biblioteca Gson para converter o body das requisições para JSON, conforme exigido pela minha API (isso pode variar na sua).
E no onCreate da MainActivity, adicione uma chamada à esse método logo no final (aproveitei para adicionar a permissão de uso de rede na thread principal, conforme explico melhor aqui):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ButterKnife.bind(this); carregarAPI(); StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); } |
Com isso, sempre que precisarmos consumir nosso webservice, basta chamar MainActivity.api.nomeDoMetodo e é isso que temos de fazer agora, substituindo aqueles testes fake por chamadas de verdade à API.
Atenção: como usaremos webapis através da Internet do smartphone, devemos ter permissão para tanto. Adiciona no AndroidManifest.xml as duas linhas logo no início, dentro da tag manifest:
1 2 3 |
<uses-permission android:name="android.permission.INTERNET" /> |
Isso resolve para smartphones com Android anterior ao 6. Para smartphones Marshmallow (6) em diante, você deverá incluir um código de verificação e solicitação de permissão do usuário a cada requisição. Eu incluirei esse código nas chamadas dos métodos abaixo, então não se preocupe.
#5 – Fazendo tudo funcionar
Agora que temos tudo pronto com nosso Retrofit, é hora de usar nosso recém-criado ClientesService através da variável api nos métodos de click dos nosso botões.
Vamos começar com o btnBuscarOnClick, que agora deverá ficar assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
private final int ACTION_BUSCA = 1; @OnClick(R.id.btnBuscar) public void btnBuscarOnClick(){ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, ACTION_BUSCA); } else { int id = Integer.parseInt(txtId.getText().toString()); //vai na API com esse id e traz o cliente Call<List<Cliente>> request = MainActivity.api.selectCliente(id); Response<List<Cliente>> response = null; try { response = request.execute(); } catch (IOException e) { e.printStackTrace(); } if (response != null && response.isSuccessful() && response.body().size() > 0) { //se encontrou um cliente com aquele id //preenche os campos do formulário com os dados do cliente Cliente cliente = response.body().get(0); txtNome.setText(cliente.nome); txtCpf.setText(cliente.cpf); } else { Toast.makeText(this, "Nenhum cliente encontrado com o id " + id, Toast.LENGTH_LONG).show(); } } } |
Note que houve uma imensa alteração aqui. Logo no início eu defino uma constante que define o número 1 para uma ação de busca, isso vai ser útil na sequência. Antes de fazer qualquer coisa no clique deste botão eu verifico se o app tem permissão de usar a Internet, caso contrário, solicita ao usuário que dê a referida permissão, passando o código desta ação de busca (1).
Caso ele tenha a permissão, o código flui naturalmente, usando o método da interface que retorna apenas um cliente por id e usando o cliente retornado para popular os campos.
Para que isto funcione, temos de sobrescrever o método onRequestPermissionsResult que é executado automaticamente após a permissão ter sido concedida ou não pelo usuário:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case ACTION_BUSCA: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { btnBuscarOnClick(); return; } break; } } Toast.makeText(this, "Sem essa permissão o app não irá funcionar. Tente novamente.", Toast.LENGTH_LONG).show(); } |
Aqui é onde usamos o código da ação de busca (1), pois todos os pedidos de permissão vão cair aqui e precisamos saber qual método executar após a permissão ser concedida. Essa permissão será requisitada apenas na primeira ação, nas subsequentes o fluxo cairá no else do código anterior a este e tudo acontecerá naturalmente pois a permissão fica salva.
Sugiro já realizar um teste aqui, para garantir que está funcionando. Acesse a API Node+MySQL no seu navegador e mande listar todos os clientes para pegar o ID de algum deles e usar no app para buscar.
Próximo passo, btnExcluirOnClick!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
private final int ACTION_EXCLUIR = 2; @OnClick(R.id.btnExcluir) public void btnExcluirOnClick(){ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, ACTION_EXCLUIR); } else { final int id = Integer.parseInt(txtId.getText().toString()); //cria o dialog de confirmação AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Você tem certeza que deseja excluir este cliente?") .setPositiveButton("Sim", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { //vai na API com esse id e exclui o cliente Call request = MainActivity.api.deleteCliente(id); Response response = null; try { response = request.execute(); } catch (IOException e) { e.printStackTrace(); Log.e("APIMYSQL", e.getMessage()); } if (response.isSuccessful()) { Toast.makeText(getApplicationContext(), "Cliente excluído com sucesso!", Toast.LENGTH_LONG).show(); txtId.setText(""); txtNome.setText(""); txtCpf.setText(""); } else { Toast.makeText(getApplicationContext(), "Nenhum cliente encontrado com o id " + id, Toast.LENGTH_LONG).show(); } } }); builder.create().show(); } } |
Aqui as mudanças não foram tão drásticas se você considerar que muita coisa é repetida em relação às permissões e à própria chamada da api em si, que é apenas um método pois o Retrofit faz todo o trabalho pela gente.
Note que defini uma nova constante para ACTION_EXCLUIR, constante essa que devemos incluir no switch/case do onRequestPermissionsResult:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case ACTION_BUSCA: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { btnBuscarOnClick(); return; } break; } case ACTION_EXCLUIR: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { btnExcluirOnClick(); return; } break; } } Toast.makeText(this, "Sem essa permissão o app não irá funcionar. Tente novamente.", Toast.LENGTH_LONG).show(); } |
Próximo passo, btnSalvarOnClick!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
private final int ACTION_SALVAR = 3; @OnClick(R.id.btnSalvar) public void btnSalvarOnClick(){ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, ACTION_SALVAR); } else { String idStr = txtId.getText().toString(); final int id = idStr.equals("") ? 0 : Integer.parseInt(idStr); Response response = null; Call<Cliente> request = null; try { if (id > 0) { // edição //PATCH na API enviando o id junto para editar request = MainActivity.api.updateCliente(id, txtNome.getText().toString(), txtCpf.getText().toString()); } else { // novo cadastro //POST na API sem enviar o id que será gerado no banco request = MainActivity.api.insertCliente(txtNome.getText().toString(), txtCpf.getText().toString()); } response = request.execute(); } catch (IOException e) { e.printStackTrace(); Log.e("APIMYSQL", e.getMessage()); } if (!response.isSuccessful()) { if (id > 0) Toast.makeText(this, "Não foi encontrado nenhum cliente com esse id para atualizar ou a atualização não foi permitida.", Toast.LENGTH_LONG).show(); else Toast.makeText(this, "Não foi possível salvar esse cliente.", Toast.LENGTH_LONG).show(); } else Toast.makeText(this, "Cliente salvo com sucesso!", Toast.LENGTH_LONG).show(); } } |
Neste método, que guarda muitas semelhanças com os anteriores, executamos o método correto da interface conforme for um insert ou update, no mais não há novidade alguma.
Não esqueça de incluir a nova constante ACTION_SALVAR no onRequestPermissionsResult:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case ACTION_BUSCA: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { btnBuscarOnClick(); return; } break; } case ACTION_EXCLUIR: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { btnExcluirOnClick(); return; } break; } case ACTION_SALVAR: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { btnSalvarOnClick(); return; } break; } } Toast.makeText(this, "Sem essa permissão o app não irá funcionar. Tente novamente.", Toast.LENGTH_LONG).show(); } |
Quando for testar o botão de salvar você receberá as mensagens de sucesso ou de fracasso, mas para ter certeza de que realmente funcionou, acesse sua API para ver se o cliente em questão foi alterado ou se apareceu um novo cliente, dependendo do seu caso.
#6 – Indo além!
Opcionalmente existem diversas melhorias que você pode fazer neste app.
Alguns apps usam o banco local (SQLite) para fazer cache dos dados da API que o app consome. Você aprende a usar o banco local neste post. Isso pode ser uma boa ideia dependendo do seu caso, apenas tomando cuidado de ter uma forma de atualizar o seu cache ou mesmo expirá-lo automaticamente.
Não criamos uma tela de listagem de todos os clientes neste tutorial. Isso geralmente é feito e a recomendação atual é usar RecyclerView para isto, um componente moderno e poderoso que permite criar belas e rápidas listagens. Neste post eu ensino como tirar um bom proveito do RecyclerView e com poucas adaptações você consegue usar neste tutorial aqui.
Não adicionei validações no formulário de cadastro/edição de cliente. Isso é super importante para garantir que somente dados íntegros sejam enviados à API. Facilmente você nota que se clicar diretamente em Salvar, sem preencher nenhum campo, mesmo assim um novo cliente vazio é cadastrado com um novo id.
Caso a API possuísse autenticação (a minha é aberta), existem algumas configurações adicionais no Retrofit para que ele envie junto o cabeçalho Authorization. Isso é melhor explicado na seção de Header Manipulation da documentação do Retrofit.
E com isso encerramos este tutorial de como consumir APIs em Android usando Retrofit e ButterKnife.
Espero que tenha gostado e que lhe seja útil!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
show de bola!
Uma duvida. Como faria para o servidor notificar o app de uma atualização em um registro no bando de dados ?
Dá pra fazer com Socket.io, conforme mostrei em um tutorial recente aqui no blog e que hoje deve sair a segunda parte usando Android. Acho que dá pra fazer com Firebase também.