Android

8 set, 2017

Consumindo APIs em Android com Retrofit e Butter Knife – Parte 02

Publicidade

Esta é a segunda parte do artigo sobre como consumir APIs em um app Android usando Retrofit e ButterKnife (veja a parte 01 aqui). Caso tenha caído aqui de pára-quedas, sugiro ler (e programar) a primeira parte primeiro, seja clicando no link anterior ou usando o sumário logo abaixo (os itens 1 a 3 são da parte 01 do artigo).

Apenas recapitulando: estou usando uma API escrita em Node.js com banco MySQL, cujo tutorial e fontes se encontram neste artigo. Esta API e seu banco estão hospedados na Umbler, que fornece MySQL gratuito para pequenos projetos.

Sobre as bibliotecas-foco desse artigo, a Retrofit permite abstrair requisições HTTP da 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:

Tela do app

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 artigo, 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):

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):

# 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:

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:

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 01 deste artigo, 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:

 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):

    @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:

<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:

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, solicite 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:

    @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!

    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:

    @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!

    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:

    @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 artigo. 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 artigo. 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 artigo, eu ensino como tirar um bom proveito do RecyclerView e com poucas adaptações você consegue usar 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 artigo de como consumir APIs em Android usando Retrofit e ButterKnife.

Espero que tenha gostado e que lhe seja útil!