Android

18 jul, 2017

CRUD em Android com SQLite e RecyclerView – Parte 02

Publicidade

Seguimos agora com a tão aguardada segunda parte do artigo CRUD em Android com SQLite e RecyclerView. Se chegou agora e ainda não fez a primeira parte, faça, pois é bem difícil de conseguir acompanhar sem ter concluído todos os preparativos iniciais.

O que é uma RecyclerView? A RecyclerView é a substituta mais moderna ao ListView que, até então, era o componente recomendado para criar listagens. No entanto, o ListView está atrelado à versão do SDK que você está rodando, é pesado e tem pouca flexibilidade. A RecyclerView é um pouco mais complicada de lidar, mas vale a pena, considerando os benefícios que se tem com ela, como veremos nesta segunda parte do artigo.

Mãos à obra!

Parte 5: Listando clientes

Primeiro, vamos criar na nossa classe ClienteDAO o método que vai retornar todos os clientes do banco de dados, como segue:

public List<Cliente> retornarTodos(){
   List<Cliente> clientes = new ArrayList<>();
   Cursor cursor = gw.getDatabase().rawQuery("SELECT * FROM Clientes", null);
   while(cursor.moveToNext()){
      int id = cursor.getInt(cursor.getColumnIndex("ID"));
      String nome = cursor.getString(cursor.getColumnIndex("Nome"));
      String sexo = cursor.getString(cursor.getColumnIndex("Sexo"));
      String uf = cursor.getString(cursor.getColumnIndex("UF"));
      boolean vip = cursor.getInt(cursor.getColumnIndex("Vip")) > 0;
      clientes.add(new Cliente(id, nome, sexo, uf, vip));
   }
   cursor.close();
   return clientes;
}

Aqui, usamos o método rawQuery do objeto SQLiteDatabase para executar uma consulta SELECT em cima de todos clientes da base, o que nos retorna um cursor. Com um laço em cima do curso, conseguimos retornar cada uma das colunas em cada uma das linhas da tabela, populando uma coleção de Clientes com essas informações.

Atenção: para pegar o índice da coluna a partir do seu nome, o mesmo deve ser informado tal qual foi criado no CREATE TABLE original. Caso não queira pegar por nome, você pode pegar por índice, desde que saiba a ordem que as colunas serão retornadas do banco (se especificar as colunas ao invés de usar SELECT *, fica mais fácil).

Agora, vamos voltar a nossa atenção ao arquivo de layout content_main.xml, que vamos editar para incluir uma lista de elementos nele, que mais tarde será populada com os clientes do banco de dados. Já que estamos usando de boas práticas em vários cantos, não usaremos aqui ListView, mas sim a versão mais moderna para listagem de elementos que é a RecyclerView, um componente gráfico independente de versão do SDK, que é atualizado constantemente, tem muito mais desempenho, mais opções visuais, de animação etc.

Para poder usar a RecyclerView, primeiro você deve adicionar algumas dependências no seu arquivo build.gradle dentro da pasta app, dentro da seção dependencies do mesmo:

compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:design:25.3.1'

Atenção: Certifique-se de que a versão das bibliotecas ‘-v7’ seja a mesma entre todos eles. No meu caso, 25.3.1, a mais recente na data que escrevo este artigo. É possível também que a dependência da biblioteca support:design já esteja adicionada nesse arquivo, nesse caso, não precisa adicionar de novo.

Para podermos criar belas listas usando RecyclerView, devemos adotar uma técnica semelhante ao que fizemos no artigo sobre ListViews personalizadas: temos de ter um layout XML que represente um único item da lista, para o mesmo ser replicado.

Sendo assim, adicione um novo layout XML na pasta de layouts chamado item_lista.xml com o layout ConstraintLayout na raiz e crie uma interface semelhante à esta:

O código XML para fazer esta interface está abaixo. Você não precisa fazer igual, mas atente aos ids dos componentes que é o mais importante no momento:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
        android:layout_height="wrap_content"
        android:layout_width="0dp"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large"
        android:id="@+id/nomeCliente"
        android:text="Nome do cliente"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/btnEdit"
        android:layout_marginRight="8dp" />
 
    <ImageButton
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:id="@+id/btnEdit"
        android:src="@android:drawable/ic_menu_edit"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp"
        app:layout_constraintRight_toLeftOf="@+id/btnDelete"
        android:layout_marginRight="8dp" />
 
    <ImageButton
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:id="@+id/btnDelete"
        android:src="@android:drawable/ic_delete"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp" />
 
</android.support.constraint.ConstraintLayout>

Agora sim, vamos para a content_main.xml. Apagamos aquele Hello World que estava lá e vamos adicionar uma RecyclerView, que fica na categoria AppCompat, da Palette do Layout Editor do Android Studio.

A RecyclerView possui uma propriedade listitem, onde devemos informar o layout XML que usaremos para os itens da mesma, no nosso caso, o item_lista.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="br.com.luiztools.androidcrud.MainActivity"
    tools:showIn="@layout/activity_main">
 
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="8dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/item_lista" />
</android.support.constraint.ConstraintLayout>

O preview da sua tela principal deve mudar também, logo após você adicionar a informação do layout da lista.

Para que os campos do item_lista.xml sejam mapeados corretamente quando carregarmos os elementos na lista, devemos criar uma classe Java extendendo ViewHolder, como a ClienteHolder.java abaixo:

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TextView;
 
public class ClienteHolder extends RecyclerView.ViewHolder {
 
    public TextView nomeCliente;
    public ImageButton btnEditar;
    public ImageButton btnExcluir;
 
    public ClienteHolder(View itemView) {
        super(itemView);
        nomeCliente = (TextView) itemView.findViewById(R.id.nomeCliente);
        btnEditar = (ImageButton) itemView.findViewById(R.id.btnEdit);
        btnExcluir = (ImageButton) itemView.findViewById(R.id.btnDelete);
    }
}

Na sequência, vamos criar o ClienteAdapter.java, que vai fazer a ligação entre os dados dos clientes e os campos do layout item_lista. Para a classe ser uma Adapter é necessário herdar RecyclerView.Adapter<ViewHolder> e implementar os métodos obrigatórios.

  • onCreateViewHolder(ViewGroup parent, int viewType): Método que deverá retornar layout criado pelo ViewHolder já inflado em uma view.
  • onBindViewHolder(ViewHolder holder, int position): Método que recebe o ViewHolder e a posição da lista. Aqui é recuperado o objeto da lista de Objetos pela posição e associado à ViewHolder. É onde a mágica acontece!
  • getItemCount(): Método que deverá retornar quantos itens há na lista. Aconselha-se verificar se a lista não está nula como no exemplo, pois ao tentar recuperar a quantidade da lista nula pode gerar um erro em tempo de execução (NullPointerException).
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import java.util.List;
 
 
public class ClienteAdapter extends RecyclerView.Adapter<ClienteHolder> {
 
    private final List<Cliente> clientes;
 
    public ClienteAdapter(List<Cliente> clientes) {
        this.clientes = clientes;
    }
 
    @Override
    public ClienteHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ClienteHolder(LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_lista, parent, false));
    }
 
    @Override
    public void onBindViewHolder(ClienteHolder holder, int position) {
        holder.nomeCliente.setText(clientes.get(position).getNome());
    }
 
    @Override
    public int getItemCount() {
        return clientes != null ? clientes.size() : 0;
    }
}

Vale ressaltar que os métodos onCreateViewHolder e onBindViewHolder não são chamados para todos os itens inicialmente, eles são chamados apenas para os itens visíveis para o usuário. Quando o usuário sobe e desce a lista, estes dois métodos são chamados novamente, associando a view reciclada ao conteúdo daquela posição que agora será visível.

Agora é a hora de voltarmos ao nosso MainActivity.java e programar a integração da RecyclerView com os dados da ClienteDAO. Primeiro, crie um método configurarRecycler, como abaixo:

RecyclerView recyclerView;
ClienteAdapter adapter;
 
private void configurarRecycler() {
   // Configurando o gerenciador de layout para ser uma lista.
   recyclerView = (RecyclerView)findViewById(R.id.recyclerView);
   LinearLayoutManager layoutManager = new LinearLayoutManager(this);
   recyclerView.setLayoutManager(layoutManager);
 
   // Adiciona o adapter que irá anexar os objetos à lista.
   ClienteDAO dao = new ClienteDAO(this);
   adapter = new ClienteAdapter(dao.retornarTodos());
   recyclerView.setAdapter(adapter);
   recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
}

E depois, chame esse configurarRecycler() no final do onCreate da MainActivity.java, para que ele seja disparado e popule inicialmente a RecyclerView com a lista de clientes do banco.

Isso deve funcionar parcialmente. Quando abrimos o app pela primeira vez, ele vai listar corretamente todos os clientes. Mas quando adicionamos um novo cliente, ele não se atualiza.

Até poderíamos resolver isso facilmente, mandando chamar novamente o configurarRecycler() a cada nova inserção, mas essa não é a maneira correta de fazer. Ao invés disso, quando um cliente novo for inserido no banco, ele também deve ser inserido na coleção de clientes que o ClienteAdapter referencia, em memória. Para isso, vamos fazer alguns ajustes.

Primeiro, vamos criar um método no ClienteDAO.java para retornar o último Cliente inserido no banco de dados. Isso porque como o ID é automático e autoincremental, precisamos saber o ID que o cliente recebeu para adicionar o objeto completo na nossa lista (usaremos esse ID mais tarde na edição e exclusão). O código é bem parecido com o de retornarTodos, mais simples até; então, dispensa explicações:

public Cliente retornarUltimo(){
   Cursor cursor = gw.getDatabase().rawQuery("SELECT * FROM Clientes ORDER BY ID DESC", null);
   if(cursor.moveToFirst()){
      int id = cursor.getInt(cursor.getColumnIndex("ID"));
      String nome = cursor.getString(cursor.getColumnIndex("Nome"));
      String sexo = cursor.getString(cursor.getColumnIndex("Sexo"));
      String uf = cursor.getString(cursor.getColumnIndex("UF"));
      boolean vip = cursor.getInt(cursor.getColumnIndex("Vip")) > 0;
      cursor.close();
      return new Cliente(id, nome, sexo, uf, vip);
   }
 
   return null;
}

Agora, vamos editar o nosso ClienteAdapter.java para ter um método que adicione novos clientes à lista:

public void adicionarCliente(Cliente cliente){
   clientes.add(cliente);
   notifyItemInserted(getItemCount());
}

Bem simples, apenas adiciona o cliente na coleção in-memory e notifica a RecyclerView que ela deve se atualizar.

Agora, no nosso click do botão de salvar, após salvar, vamos pegar o último elemento e adicioná-lo no adapter usando o método que já está pronto nele, que inclusive atualiza o RecyclerView na sequência. Note que o trecho de código abaixo é o último bloco do onClick do btnSalvar e eu apenas coloquei duas linhas novas no início do if:

//salvando os dados
ClienteDAO dao = new ClienteDAO(getBaseContext());
boolean sucesso = dao.salvar(nome, sexo, uf, vip);
if(sucesso) {
   Cliente cliente = dao.retornarUltimo();
   adapter.adicionarCliente(cliente);
 
   //limpa os campos
   txtNome.setText("");
   rgSexo.setSelected(false);
   spnEstado.setSelection(0);
   chkVip.setChecked(false);
 
   Snackbar.make(view, "Salvou!", Snackbar.LENGTH_LONG)
           .setAction("Action", null).show();
   findViewById(R.id.includemain).setVisibility(View.VISIBLE);
   findViewById(R.id.includecadastro).setVisibility(View.INVISIBLE);
   findViewById(R.id.fab).setVisibility(View.VISIBLE);
}else{
   Snackbar.make(view, "Erro ao salvar, consulte os logs!", Snackbar.LENGTH_LONG)
           .setAction("Action", null).show();
}

Com isso, as duas primeiras letras do CRUD estão prontas: o C de Create e o R de Read.

Agora é hora de fazer funcionar aqueles dois botões à direita dos nomes!

Parte 6: atualizando clientes

Agora, vamos programar a atualização de clientes. A ideia é a seguinte: o usuário clica no botão de editar, daí abrimos a mesma tela de cadastro, mas com os dados do cliente que está sendo editado já preenchido. Daí, quando o usuário clica em Salvar, ele faz um update no banco ao invés de um insert.

Para fazer isso, primeiro vamos editar o nosso ClienteDAO para incluir uma sobrecarga do método salvar que recebe um id por parâmetro, informando que é uma atualização ao invés de uma inserção. Não apenas isso, mas achei mais interessante jogar fora aquele método salvar antigo e no lugar adicionei as duas versões abaixo (com e sem id):

public boolean salvar(String nome, String sexo, String uf, boolean vip){
   return salvar(0, nome, sexo, uf, vip);
}
 
public boolean salvar(int id, String nome, String sexo, String uf, boolean vip){
   ContentValues cv = new ContentValues();
   cv.put("Nome", nome);
   cv.put("Sexo", sexo);
   cv.put("UF", uf);
   cv.put("Vip", vip ? 1 : 0);
   if(id > 0)
      return gw.getDatabase().update(TABLE_CLIENTES, cv, "ID=?", new String[]{ id + "" }) > 0;
   else
      return gw.getDatabase().insert(TABLE_CLIENTES, null, cv) > 0;
}

Muito menor e mais inteligente!

Agora, precisamos editar nossa classe ClienteAdapter.java, que é quem possui a lógica dos botões para programar o click do btnEditar. Primeiro, vamos começar adicionando este método que permite obter uma Activity a partir de uma View qualquer, algo útil, considerando que o ClienteAdapter não é uma Activity:

private Activity getActivity(View view) {
   Context context = view.getContext();
   while (context instanceof ContextWrapper) {
      if (context instanceof Activity) {
        return (Activity)context;
      }
      context = ((ContextWrapper)context).getBaseContext();
   }
   return null;
}

Agora, ainda no ClienteAdapter.java, mas dentro do método onBindViewHolder, adicione o seguinte bloco de código no final do método:

holder.btnEditar.setOnClickListener(new Button.OnClickListener() {
   @Override
   public void onClick(View v) {
      Activity activity = getActivity(v);
      Intent intent = activity.getIntent();
      intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
      intent.putExtra("cliente", cliente);
      activity.finish();
      activity.startActivity(intent);
   }
});

Isso vai fazer com que, ao clicar no botão de editar de alguma das linhas da RecyclerView, seja disparado um “refresh” na Activity atual, passando o cliente que deve ser editado feito Extra.

Aproveitando que estamos aqui no ClienteAdapter.java, vamos adicionar um último método pra ele permitir a atualização fácil e rápida de clientes na RecyclerView mais tarde, no final desta etapa:

public void atualizarCliente(Cliente cliente){
   clientes.set(clientes.indexOf(cliente), cliente);
   notifyItemChanged(clientes.indexOf(cliente));
}

Apenas adicione o método acima na ClienteAdapter.java, que tem mecânica semelhante com outros método que também adicionamos por lá, e vamos continuar!

Voltando à MainActivity.java, adicione o seguinte bloco de código na classe:

Cliente clienteEditado = null;
 
private int getIndex(Spinner spinner, String myString)
{
   int index = 0;
   for (int i=0;i<spinner.getCount();i++){
      if (spinner.getItemAtPosition(i).toString().equalsIgnoreCase(myString)){
         index = i;
         break;
      }
   }
   return index;
}

Basicamente, aqui temos uma variável que usaremos pra guardar o cliente que será editado e um método utilitário para selecionarmos o item de um Spinner a partir de seu valor, que usaremos mais tarde.

Agora, ainda no MainActivity.java, mas dentro do método onCreate, adicione esta verificação logo após o setContentView:

//verifica se começou agora ou se veio de uma edição
Intent intent = getIntent();
if(intent.hasExtra("cliente")){
  findViewById(R.id.includemain).setVisibility(View.INVISIBLE);
  findViewById(R.id.includecadastro).setVisibility(View.VISIBLE);
  findViewById(R.id.fab).setVisibility(View.INVISIBLE);
  clienteEditado = (Cliente) intent.getSerializableExtra("cliente");
  EditText txtNome = (EditText)findViewById(R.id.txtNome);
  Spinner spnEstado = (Spinner)findViewById(R.id.spnEstado);
  CheckBox chkVip = (CheckBox)findViewById(R.id.chkVip);
 
  txtNome.setText(clienteEditado.getNome());
  chkVip.setChecked(clienteEditado.getVip());
  spnEstado.setSelection(getIndex(spnEstado, clienteEditado.getUf()));
  if(clienteEditado.getSexo() != null){
     RadioButton rb;
     if(clienteEditado.getSexo().equals("M"))
        rb = (RadioButton)findViewById(R.id.rbMasculino);
     else
        rb = (RadioButton)findViewById(R.id.rbFeminino);
     rb.setChecked(true);
  }
}

Isso faz com que, se estiver vindo um Cliente no Intent que disparou esta Activity, vamos abrir o app na tela de edição ao invés de listagem, já carregando os campos com os valores atuais do cliente.

Para finalizar, temos que fazer com que o botão de salvar seja mais inteligente do que atualmente é. Hoje, ele sempre manda salvar um novo cliente, mas agora queremos que ele veja se estamos editando um novo cliente (através da variável clienteEditado) ou se estamos salvando um novo cliente. No código abaixo, eu apenas alterei poucas linhas dentro do onClick do btnSalvar, colocando um if onde antes era apenas uma chamada a dao.salvar:

//salvando os dados
ClienteDAO dao = new ClienteDAO(getBaseContext());
boolean sucesso;
if(clienteEditado != null)
   sucesso = dao.salvar(clienteEditado.getId(), nome, sexo, uf, vip);
else
   sucesso = dao.salvar(nome, sexo, uf, vip);
 
if(sucesso) {
   Cliente cliente = dao.retornarUltimo();
   if(clienteEditado != null){
      adapter.atualizarCliente(cliente);
      clienteEditado = null;
   }else
       adapter.adicionarCliente(cliente);
//continua normal com a limpeza de campos aqui embaixo

Isso é o suficiente para fazer com que a nossa edição funcione tão bem quanto as demais operações do nosso app de CRUD.

Editando cliente

E com isso terminamos a letra U do nosso CRUD!

Parte 7: removendo clientes

Agora vamos programar a exclusão de clientes. A ideia é a seguinte: o usuário clica no botão de excluir, daí pedimos uma confirmação e, se ele confirmar, mandamos o ClienteDAO excluir o cliente e removemos o objeto do ClienteAdapter, notificando a lista para que seja atualizada.

Para fazer isso, primeiro vamos editar nossa classe ClienteDAO.java para que tenha um método de exclusão de Cliente:

public boolean excluir(int id){
   return gw.getDatabase().delete(TABLE_CLIENTES, "ID=?", new String[]{ id + "" }) > 0;
}

Agora, vamos editar a classe ClienteAdapter.java para ter um método de exclusão lá também:

public void removerCliente(Cliente cliente){
   int position = clientes.indexOf(cliente);
   clientes.remove(position);
   notifyItemRemoved(position);
}

Aqui pegamos a posição do cliente que queremos excluir, removemos ele da lista e depois notificamos a RecyclerView.

Agora, vamos juntar as pontas mexendo no onBindViewHolder do ClienteAdapter.java  novamente, adicionando o listener ao click do btnExcluir presente nos itens da lista. Aqui, temos de exibir um popup de confirmação para o usuário e, em caso afirmativo, realizar a exclusão de facto, chamando os métodos que criamos anteriormente:

final Cliente cliente = clientes.get(position);
holder.btnExcluir.setOnClickListener(new Button.OnClickListener() {
   @Override
   public void onClick(View v) {
      final View view = v;
      AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
      builder.setTitle("Confirmação")
             .setMessage("Tem certeza que deseja excluir este cliente?")
             .setPositiveButton("Excluir", new DialogInterface.OnClickListener() {
                 @Override
                 public void onClick(DialogInterface dialog, int which) {
                    Cliente cliente = clientes.get(index);
                    ClienteDAO dao = new ClienteDAO(view.getContext());
                    boolean sucesso = dao.excluir(cliente.getId());
                    if(sucesso) {
                       removerCliente(cliente);
                       Snackbar.make(view, "Excluiu!", Snackbar.LENGTH_LONG)
                               .setAction("Action", null).show();
                    }else{
                        Snackbar.make(view, "Erro ao excluir o cliente!", Snackbar.LENGTH_LONG)
                                .setAction("Action", null).show();
                    }
           }
         })
         .setNegativeButton("Cancelar", null)
         .create()
         .show();
   }
});

E o mais incrível: funciona!

Exclusão funcionando

E com isso terminamos a última letra do CRUD. A D, de Delete!

Espero que tenham gostado. Deu uma trabalheira fazer, mas tenho certeza de que ficou uma ótima referência para o pessoal que está querendo fazer um CRUD completo com boas práticas e componentes modernos.

Precisando de referências mais completas quanto ao uso do SQLiteDatabase, consulte a documentação oficial.

Tendo qualquer dúvida, chame aí nos comentários!