Fazia tempo que este post 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 post 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 tutorial primeiro.
Avisos feitos, vamos começar o tutorial!
Veremos neste tutorial:
- Criando e explorando o projeto Basic
- Criando a tela de cadastro/edição
- Preparando a persistência de dados
- Cadastrando Clientes
- Listando Clientes
- Atualizando Clientes
- Removendo Clientes
Parte 1: 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@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:
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 |
<?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 email, 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 iso pelo próprio editor de XML do Android Studio na propriedade app:srcCompat do FloatingActionButton como abaixo:
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):
1 2 3 4 5 6 |
<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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?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):
O código desta tela pode ser obtido abaixo:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
<?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:
1 2 3 4 5 6 7 8 9 10 |
<?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:
1 2 3 4 5 6 |
<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:
1 2 3 4 5 6 7 8 9 10 11 |
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:
1 2 3 4 5 6 7 8 9 10 11 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 pra 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, extendendo as funcionalidades da SQLiteOpenHelper nativa do Android:
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 |
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 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 a mesma.
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):
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 |
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:
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 |
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 de 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 muito mais tarde neste tutorial.
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 acesso à dados para uso das telas mais tarde:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 tutorial, 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:
1 2 3 4 5 6 7 8 9 10 |
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):
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 |
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 tutorial 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 dos eu 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”.
A segunda parte deste tutorial pode ser conferida neste link.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
No ListView tinhamos o setOnItemClickListener. E no recyclerView. como faço ?
Quero dar um clique e executar alguma acão
Por padrão você deve definir o onClick em que cada elemento dentro do item que quiser que seja clicado, assim como fiz nesse tutorial. Mas aqui tem algumas ideias de como fazer para o item inteiro: https://stackoverflow.com/questions/24471109/recyclerview-onclick
Você tem algum post com exemplo pratico de SQLite utilizando a nova biblioteca de persistência Room do Android? essa nova biblioteca foi anunciada no Google I/O ’17 e esta sendo a mais recomendada pelo Google no uso de SQLite, já rodei na web, mas não vejo um exemplo bem pratico e completo de CRUD utilizando a Room… Fica a sugestão para um novo Post do seu site, e afinal, parabéns pelo site, possui postagens com conteúdo pratico e simples de implementar.
Infelizmente não tive tempo de estudar a Room ainda, mas obrigado pela dica.
muito bom
Fico feliz em saber que um tutorial tão antigo ainda foi útil. 🙂