Android

13 jul, 2017

CRUD em Android com SQLite e RecyclerView – Parte 01

Publicidade

Fazia tempo que este artigo estava na minha pauta: como fazer um CRUD em Android com SQLite. Mas antes de começar, vamos a algumas definições:

O que é um CRUD? É um acrônimo para Create, Read, Update e Delete: as quatro operações elementares com bancos de dados relacionais.

O que é SQLite? É o banco de dados compacto mais utilizado no mundo e que já vem com suporte nativo na plataforma Android, como banco de dados local nos smartphones.

Se é a primeira vez que está criando um app para Android, sugiro ler primeiro este artigo aqui, bem mais introdutório: Android Studio Tutorial.

Certifique-se antes de começar de que você possui o Constraint Layout disponível no seu Android Studio, pois usaremos ele aqui como gerenciador de layout. Se você nunca lidou com ele antes, dê uma olhada neste artigo primeiro.

Avisos feitos, vamos começar!

Parte 01: Criando e explorando o projeto Basic

Crie um novo projeto no Android Studio com o nome de AndroidCRUD. Durante o assistente de criação do projeto, escolha como Activity inicial a Basic Activity, aquela que tem o botão de + no canto direito. O resto deixe tudo padrão e avance até o final.

A Basic Activity adiciona uma série de elementos prontos que podemos customizar conforme as nossas necessidades. Nossa estrutura de pastas já começa assim:

Se abrirmos a MainActivity, veremos que ela já possui um evento onCreate com o seguinte código:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
 
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }

Aqui, definimos o arquivo XML de layout que é o activity_main.xml: uma toolbar que ficará no topo da Activity e um botão flutuante (Floating Button) que, quando clicado, vai disparar uma mensagem genérica na Snackbar (uma barra inferior, tipo um Toast mais moderno).

Mais abaixo, temos o código de invocação do menu que fica no canto superior direito da toolbar azul, o que não vem ao caso olharmos agora.

Já na pasta layout, temos o activity_main.xml, que foi referenciado no código Java anterior, e o content_main.xml. Isso porque, se olharmos o código do activity_main.xml logo abaixo, poderemos notar que ele usa uma tag include para o outro XML, permitindo um reaproveitamento de elementos de layout, assim como fazemos em tecnologias web:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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"
    tools:context="br.com.luiztools.androidcrud.MainActivity">
 
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">
 
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />
 
    </android.support.design.widget.AppBarLayout>
 
    <include layout="@layout/content_main" />
 
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />
 
</android.support.design.widget.CoordinatorLayout>

Este XML serve como uma página-mestra do app, garantindo uma uniformidade entre as telas, definindo elementos básicos, como a toolbar no topo e o floating button no canto inferior direito. Falando dele, por padrão, ele veio com um ícone de e-mail, mas podemos mudar isso facilmente, pois faremos um cadastro de clientes, então, queremos um sinal de adição como ícone. Note como faço isso pelo próprio editor de XML do Android Studio na propriedade app:srcCompat do FloatingActionButton como abaixo:

Trocando o ícone

Ainda no XML activity_main, na tag include, vamos colocar um id nela para que possamos alterar o XML que ela referencia através de nosso código Java mais tarde, deixe-o como abaixo (incluindo o visibility):

    <include
        android:id="@+id/includemain"
        layout="@layout/content_main"
        android:visibility="visible"/>

Já o XML content_main.xml contém o “miolo”; a área central, a tela – aqui, no caso, apenas com um Hello World, que mais tarde iremos alterar:

<?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">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
</android.support.constraint.ConstraintLayout>

Agora que já criamos e exploramos a estrutura básica do projeto, vamos em frente!

Parte 2: Criando a tela de Cadastro/Edição

Agora que entendemos que a activity_main.xml será o “esqueleto” de todas telas, e que devemos criar apenas os arquivos XML de “miolo”, podemos adicionar um novo arquivo de layout na pasta correspondente com o nome de content_cadastro e com o root tag ConstraintLayout. Configure a aparência desse layout para que represente um formulário de cadastro como abaixo (ou similar):

Layout de cadastro

O código desta tela pode ser obtido abaixo:

<?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">
 
    <EditText
        android:id="@+id/txtNome"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="8dp"
        android:ems="10"
        android:hint="Digite o nome do cliente"
        android:inputType="textPersonName"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sexo: "
        android:id="@+id/textView"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginTop="8dp"
        android:layout_marginLeft="8dp"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large"
        app:layout_constraintTop_toBottomOf="@+id/txtNome"
        app:layout_constraintRight_toLeftOf="@+id/rgSexo"
        android:layout_marginStart="8dp" />
 
    <RadioGroup
        android:id="@+id/rgSexo"
        android:orientation="horizontal"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/textView"
        android:layout_marginTop="8dp"
        app:layout_constraintTop_toBottomOf="@+id/txtNome">
 
        <RadioButton
            android:id="@+id/rbMasculino"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Masculino"/>
 
        <RadioButton
            android:id="@+id/rbFeminino"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Feminino"/>
 
    </RadioGroup>
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="22dp"
        android:text="UF: "
        android:id="@+id/textView2"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large"
        android:layout_marginTop="8dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintTop_toBottomOf="@+id/rgSexo"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/spnEstado"
        android:layout_marginStart="8dp" />
 
    <Spinner
        android:id="@+id/spnEstado"
        android:layout_width="0dp"
        android:layout_height="26dp"
        android:entries="@array/estados"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="8dp"
        app:layout_constraintTop_toBottomOf="@+id/rgSexo"
        app:layout_constraintLeft_toRightOf="@+id/textView2" />
 
    <CheckBox
        android:id="@+id/chkVip"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="8dp"
        android:text="Este cliente é VIP"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp" />
 
    <Button
        android:id="@+id/btnCancelar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Cancelar"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        android:layout_marginStart="8dp" />
 
    <Button
        android:id="@+id/btnSalvar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Salvar"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintLeft_toRightOf="@+id/btnCancelar"
        android:layout_marginLeft="8dp"
        app:layout_constraintHorizontal_bias="0.48" />
 
</android.support.constraint.ConstraintLayout>

Alguns pontos a se considerar aqui são o uso de um RadioGroup por fora dos RadioButtons, para garantir que apenas um deles seja selecionável. E o uso de um Spinner para guardar estados, estes por sua vez devem ser armazenados em um estados.xml dentro da pasta res/values:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="estados">
        <item>RS</item>
        <item>SC</item>
        <item>PR</item>
    </string-array>
</resources>

Agora, voltando à tela activity_main.xml, vamos adicionar um novo include logo abaixo do anterior, referenciando a content_cadastro.xml, mas com uma visibility oculta:

    <include
        android:id="@+id/includecadastro"
        layout="@layout/content_cadastro"
        android:visibility="invisible"/>

A ideia é que apenas um dos includes seja visível por vez, iniciando com o id includemain e depois trocando para o includecadastro via código Java, quando o usuário clicar no FloatingActionButton. Para que essa transição ocorra, mude o código do clique do FloatingActionButton na MainActivity.java para o código abaixo, que apenas esconde o includemain (e o botão) e exibe o includecadastro:

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
       findViewById(R.id.includemain).setVisibility(View.INVISIBLE);
       findViewById(R.id.includecadastro).setVisibility(View.VISIBLE);
       findViewById(R.id.fab).setVisibility(View.INVISIBLE);
    }
});

Para concluir esta etapa de navegação, a tela de cadastro possui um botão de cancelar que não deve fazer nada especial exceto voltar para a tela anterior. Para programar essa transição, vamos incluir o trecho que manipula o onClick do btnCancelar dentro do onCreate da MainActivity.java:

Button btnCancelar = (Button)findViewById(R.id.btnCancelar);
btnCancelar.setOnClickListener(new Button.OnClickListener() {
   @Override
   public void onClick(View v) {
      findViewById(R.id.includemain).setVisibility(View.VISIBLE);
      findViewById(R.id.includecadastro).setVisibility(View.INVISIBLE);
      findViewById(R.id.fab).setVisibility(View.VISIBLE);
   }
});

E para já deixar nosso botão de Salvar parcialmente pronto, vamos criar o código abaixo que manipula o onClick do btnSalvar, também na onCreate da MainActivity.java:

Button btnSalvar = (Button)findViewById(R.id.btnSalvar);
btnSalvar.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
      Snackbar.make(view, "Salvando...", 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);
   }
});

Com isso encerramos a criação da tela de cadastro/edição. Ok, não fizemos nada ainda referente à edição, mas você vai entender mais para frente.

Parte 3: Preparando a persistência de dados

Agora que temos as telas funcionando, com suas devidas posições e o onClick do botão de Salvar apenas esperando pelo código final, vamos programar algumas classes Java que vão cuidar da parte de persistência de dados no SQLite.

Primeiro, adicione no seu package principal do projeto uma classe DbHelper como abaixo, que cuidará do script de criação e atualização do banco de dados, estendendo as funcionalidades da SQLiteOpenHelper nativa do Android:

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
 
public class DbHelper extends SQLiteOpenHelper {
 
    private static final String DATABASE_NAME = "Crud.db";
    private static final int DATABASE_VERSION = 1;
    private final String CREATE_TABLE = "CREATE TABLE Clientes (ID INTEGER PRIMARY KEY AUTOINCREMENT, Nome TEXT NOT NULL, Sexo TEXT, UF TEXT NOT NULL, Vip INTEGER NOT NULL);";
 
    public DbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
 
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE);
    }
 
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 
    }
}

O método onCreate dessa classe será chamado automaticamente na primeira vez que for realizada uma conexão com o banco de dados, criando-o com uma única tabela ‘Clientes’, conforme o SQL informado em uma String final. Os demais parâmetros posicionados como final no topo da classe são auto-explicativos. Esta é a única responsabilidade que daremos para ela.

Agora, crie uma segunda classe que vai representar o nosso cliente de banco de dados, que chamaremos DbGateway (conforme o Design Pattern Gateway), que fará as conexões nesta base que acabamos de codificar (por ora, apenas isso):

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
 
public class DbGateway {
 
    private static DbGateway gw;
    private SQLiteDatabase db;
 
    private DbGateway(Context ctx){
        DbHelper helper = new DbHelper(ctx);
        db = helper.getWritableDatabase();
    }
 
    public static DbGateway getInstance(Context ctx){
        if(gw == null)
            gw = new DbGateway(ctx);
        return gw;
    }
 
    public SQLiteDatabase getDatabase(){
        return this.db;
    }
}

Nesse DbGateway, eu também usei o Design Pattern Singleton para garantir que exista apenas um cliente de banco de dados único para todo o meu app, uma vez que o SQLite não trabalha muito bem com concorrência e porque múltiplas conexões poderiam consumir recursos demais.

Vamos criar também uma nova classe no nosso projeto, uma que espelhe a tabela Clientes do banco de dados, que chamaremos de Cliente.java, nosso data object:

public class Cliente implements Serializable {
 
    private int id;
    private String nome;
    private String sexo;
    private String uf;
    private boolean vip;
 
    public Cliente(int id, String nome, String sexo, String uf, boolean vip){
        this.id = id;
        this.nome = nome;
        this.sexo = sexo;
        this.uf = uf;
        this.vip = vip;
    }
 
    public int getId(){ return this.id; }
    public String getNome(){ return this.nome; }
    public String getSexo(){ return this.sexo; }
    public boolean getVip(){ return this.vip; }
    public String getUf(){ return this.uf; }
 
    @Override
    public boolean equals(Object o){
        return this.id == ((Cliente)o).id;
    }
 
    @Override
    public int hashCode(){
        return this.id;
    }
}

Nada demais aqui. Apenas um bando de atributos e métodos para usar essa classe como uma estrutura de dados de cliente simples. Os métodos sobrescritos serão usados mais tarde neste artigo.

Agora, para finalizar nossa preparação da persistência de dados, vamos criar uma última classe usando o Design Pattern Data Access Object, a ClienteDAO.java, que é a classe responsável por fazer a tradução dos objetos para o banco de dados e vice-versa, abstraindo o acesso à dados para uso das telas mais tarde:

import android.content.Context;
 
public class ClienteDAO {
 
    private final String TABLE_CLIENTES = "Clientes";
    private DbGateway gw;
 
    public ClienteDAO(Context ctx){
        gw = DbGateway.getInstance(ctx);
    }
}

Vamos adicionar novos métodos nessa classe nas etapas seguintes deste artigo, para de fato fazer as operações do nosso CRUD. Por ora, ela terá apenas um construtor que pega a instância única de DbGateway e deixa guardada em uma variável local para uso posterior.

Você notou que no ClienteDAO e no DbGateway precisamos de um objeto Context? Essa é uma necessidade do SQLite, saber qual o Context em que ele está sendo manipulado para questões como permissões entre outras da arquitetura do Android. Passaremos esse Context facilmente mais tarde.

Parte 4: Cadastrando clientes

Agora que temos tanto as telas, quanto à persistência de dados nos esperando, vamos finalmente fazer a nossa tela de cadastro funcionar!

Dentro da classe ClienteDAO.java, crie o seguinte método, que usa a DbGateway.java para pegar a conexão atual com o banco de dados e executar um insert no SQLite com os parâmetros recebidos:

public boolean salvar(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);
   return gw.getDatabase().insert(TABLE_CLIENTES, null, cv) > 0;
}

Aqui usei as boas práticas recomendadas na documentação oficial do Android, onde diz que para INSERTs devemos usar o método insert informando o nome da tabela e um map de content values com as colunas e valores que queremos inserir.

Agora, no click do botão de Salvar da content_cadastro, vamos chamar essa nossa classe ClienteDAO.java para executar o método salvar (abaixo eu substituo o bloco inteiro original):

Button btnSalvar = (Button)findViewById(R.id.btnSalvar);
btnSalvar.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
      //carregando os campos
      EditText txtNome = (EditText)findViewById(R.id.txtNome);
      Spinner spnEstado = (Spinner)findViewById(R.id.spnEstado);
      RadioGroup rgSexo = (RadioGroup)findViewById(R.id.rgSexo);
      CheckBox chkVip = (CheckBox)findViewById(R.id.chkVip);
 
      //pegando os valores
      String nome = txtNome.getText().toString();
      String uf = spnEstado.getSelectedItem().toString();
      boolean vip = chkVip.isChecked();
      String sexo = rgSexo.getCheckedRadioButtonId() == R.id.rbMasculino ? "M" : "F";
 
      //salvando os dados
      ClienteDAO dao = new ClienteDAO(getBaseContext());
      boolean sucesso = dao.salvar(nome, sexo, uf, vip);
      if(sucesso) {
         //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();
      }
   }
});

Separei esse código em três grandes blocos: no primeiro, apenas referencio localmente os widgets da interface gráfica. No segundo bloco, carrego variáveis locais com os valores de cada widget. E no terceiro, envio para o ClienteDAO realizar o insert retornando se funcionou ou não, retornando à tela anterior.

Se tudo deu certo, você deve conseguir salvar os dados com sucesso, mas não conseguirá vê-los depois de salvo. No artigo de testes com Android Studio e no de Engenharia Reversa eu ensino como você pode pegar o arquivo do banco de dados SQLite dentro do simulador Android. No entanto, isso não resolve o nosso problema, que é o de não ter programado a listagem (SELECT) de clientes ainda, que é o que faremos na segunda parte deste tutorial ainda esta semana, juntamente com as demais letras do CRUD.

Atenção: caso tenha criado seu banco com algum problema ou já tenha enchido o mesmo com muito lixo e queira começar do zero, você pode ir nas configurações do seu Android (tanto o simulador quanto um dispositivo físico) e limpar os dados do aplicativo. Se isso não resolver, desinstale o app  e mande rodar novamente pelo Android Studio que ele instalará “zerado”.