Este post é antigo, caso queira ler um tutorial novo com exemplo completo de CRUD usando SQLite em Android, consulte este post aqui!
Hoje o post irá tratar de um assunto muito importante: persistência de dados. Todas as aplicações que lidam com um grande volume de dados devem ter algum mecanismo de persistência, geralmente um banco de dados, para garantir a segurança, confiabilidade e integridade dos mesmos. O Android possui um mecanismo nativo, chamado de SQLite (o mesmo utilizado pelo Corona SDK, iOS e muitas outras plataformas móveis) que provê funcionalidades de banco de dados para suas aplicações. Iremos ver algumas dessas funcionalidades neste post, de uma maneira bem simples, mas que servirá como base para estudos futuros.
Veremos neste artigo:
- Banco de dados no Android
- Base de dados SQLite
- Estrutura básica da classe ContextoDados
- Lendo dados da base
- Escrevendo no banco de dados
- Conclusões
Banco de dados no Android
Para cumprir com muitas das atividades oferecidas pelos celulares modernos, como busca de contatos, eventos, e tarefas, o sistema operacional e as aplicações estar aptos a manter e rastrear grandes quantidades de dados. A maior parte destes dados está estruturado como uma planilha, na forma de linhas e colunas. Cada aplicação Android é como uma ilha por si só, e cada aplicação somente consegue ler e escrever dados criados por ela mesma, mas muitas vezes é necessário compartilhar dados além de suas fronteiras. Existem classes e interfaces Java para que o Android possa se comunicar com a base de dados relacional SQLite. Estes recursos suportam uma implementação de SQL rica o suficiente para qualquer coisa que você queira em uma aplicação mobile, incluindo facilidades como cursores.
Caso esteja buscando por acesso remoto à banco de dados, dê uma olhada nesse post.
Base de Dados SQLite
Dados são melhores armazenados em um formato de base de dados relacional se ela puder incluir muitas instâncias de um mesmo tipo de dado. Pegue uma lista de contatos, por exemplo. Existem muitos contatos, todos com os mesmos tipos de informação (endereço, telefone, etc). Cada “linha” de dados armazena informações sobre uma pessoa diferente, enquanto cada “coluna” armazena um atributo específico de cada pessoa: nomes em uma coluna, endereços em outra coluna, e telefone em uma terceira.
Android usa a engine de base de dados SQLite, uma auto-contida, engine transacional que não requer um processo de servidor para funcionar. Ele é usado por muitas aplicações e ambientes além de Android, e é desenvolvido por uma grande comunidade.
O processo que inicia a operação de base de dados, como um SELECT ou UPDATE, faz o trabalho necessário de leitura e escrita no disco que contém a base de dados visando completar a requisição. Com SQLite, a base de dados é um simples arquivo no disco. Todas as estruturas de dados formando uma base de dados relacional – tabelas, views, índices, etc. – estão contidas neste arquivo.
SQLite não é um projeto do Google, embora o Google contribua com o mesmo. SQLite tem um time internacional de desenvolvedores dedicados a melhorar as capacidades do software e sua confiabilidade. Alguns desses desenvolvedores trabalham full time no projeto.
Confiança é a característica chave do SQLite. Mais de metade do código do projeto é devotado à testes. A biblioteca é desenvolvida para lidar com muitos tipos de falhas de sistema, como pouca memória, erros de disco, e falhas de energia. Em nenhum caso a base de dados pode ficar em um estado irreparável: isto é uma grande preocupação em um telefone onde muitos dados críticos são armazenados em bases de dados. Se a base de dados fosse suscetível a corrupção de arquivos, o telefone estaria em sérios apuros quando a bateria acabasse…
Este post irá tratar dos percalços para se criar uma aplicação muito simples que consuma dados no SQLite e não se aterá à sintaxe SQL propriamente. Muito pouco de orientação à objetos e arquitetura de software será usado aqui, mantendo toda a lógica de acesso à dados em uma única classe chamada ContextoDados.java como segue (ela ainda não está completa, iremos construindo-a ao longo do post):
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 |
import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteCursorDriver; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQuery; import android.util.Log; public class ContextoDados extends SQLiteOpenHelper { /** O nome do arquivo de base de dados*/ private static final String NOME_BD = "Agenda"; /** A versão da base de dados */ private static final int VERSAO_BD = 1; /** Mantém rastreamento do contexto da aplicação */ private final Context contexto; public ContextoDados(Context context) { super(context, NOME_BD, null, VERSAO_BD); this.contexto = context; } @Override public void onCreate(SQLiteDatabase db) { String[] sql = contexto.getString(R.string.ContextoDados_onCreate).split("n"); db.beginTransaction(); try { // Cria a tabela e testa os dados ExecutarComandosSQL(db, sql); db.setTransactionSuccessful(); } catch (SQLException e) { Log.e("Erro ao criar as tabelas e testar os dados", e.toString()); } finally { db.endTransaction(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Isto é apenas didático, não precisamos deste método nesse post } /** * Executa todos os comandos SQL passados no vetor String[] * @param db A base de dados onde os comandos serão executados * @param sql Um vetor de comandos SQL a serem executados */ private void ExecutarComandosSQL(SQLiteDatabase db, String[] sql) { for( String s : sql ) if (s.trim().length()>0) db.execSQL(s); } } |
Estrutura Básica da classe ContextoDados
Em nosso exemplo, a classe ContextoDados encapsula completamente toda a lógica SQL necessária para trabalhar com a base de dados. Todas as outras classes na aplicação não possuem acesso aos dados diretamente, usando ContextoDados como um acesso central ao banco. Esta é uma prática de programação interessante que pode ser aplicada nas suas aplicações Android que utilizem base de dados.
Antes de aprofundarmo-nos nos detalhes da aplicação em si, é importante entender o funcionamento da classe ContextoDados. Esta classe herda de SQLiteOpenHelper, e sobrescreve (override) os métodos onCreate e onUpgrade. O método onCreate é automaticamente chamado quando a aplicação roda pela primeira vez; sua tarefa é criar a base de dados. Como novas versões da aplicação podem ser lançadas, a base de dados pode ser atualizada também, uma tarefa que dispara o método onUpgrade. Quando você entrega uma nova versão da base de dados, você também deve incrementar a versão, como irei explicar rapidamente a seguir.
E por fim, o método ExecutarComandosSQL é um método para facilitar a execução de comandos SQL múltiplos, que apenas chama os comandos nativos do Android para execução de comandos SQL.
A classe ContextoDados utiliza uma string constante (que fica no arquivo strings.xml): R.string.ContextoDados_onCreate com o scripts de criação do banco de dados. Esta string está abaixo e não passa de um SQL comum, que você já deve estar acostumado e que poderia estar em variáveis String normais:
1 2 3 |
<string name="ContextoDados_onCreate">CREATE TABLE Contatos (ID INTEGER PRIMARY KEY AUTOINCREMENT, Nome TEXT, Telefone TEXT, Endereco TEXT);</string> |
Lendo dados da base
Existem muitas maneiras de ler dados de uma base SQL, mas todos eles fazem uma sequência básica de operações:
- Cria um comando SQL que descreve os dados que você deseja retornar
- Executa o comando na base de dados
- Mapeia os dados SQL resultantes em uma estrutura de dados que a linguagem que você está utilizando possa entender.
Este processo pode ser muito complexo no caso de um software de mapeamento objeto relacional, ou relativamente simples quando escrevendo as consultas diretamente em sua aplicação. A diferença é a fragilidade. Ferramentas complexas de ORM protegem seu código das complexidades inerentes às bases de dados e o mapeamento de objetos, movendo esta complexidade para eles mesmos. O resultado é um código mais robusto face às alterações na base, mas ao custo de mais complexidade na configuração e manutenção do ORM.
A iniciativa de escrever consultas diretamente na sua aplicação trabalha bem somente para projetos bem pequenos que não irão mudar muito com o passar do tempo. Aplicações com código de base de dados são muito frágeis devido às alterações da mesma, pois todo o código que referenciava um elemento alterado deve ser examinado e potencialmente reescrito.
Uma técnica “meio-termo” é capturar toda a lógica de base de dados em um grupo de objetos cujo único propósito é traduzir as requisições da aplicação em requisições de banco de dados e entregar os resultados de volta para a aplicação. Esta opção é a utilizada em nossa aplicação de teste utilizando a classe ContextoDados.java.
Android nos dá a habilidade de personalizar cursores, e eu usarei essa característica para reduzir dependências de código escondendo toda a informação sobre uma operação de dados específica dentro d eum cursor personalizado. Cada cursor personalizado é uma classe dentro da classe ContextoDados, no nosso caso, somente o ContatosCursor.
Desta forma, criei um método RetornarContatos, cuja função é retornar um ContatosCursor preenchido com contatos da base de dados. O usuário pode escolher (através de um simples parâmetro) para ordenar os contatos por Nome crescente ou decrescente (este código vai dentro da classe ContextoDados criada anteriormente):
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 |
/** Retorna um ContatosCursor ordenado * @param critério de ordenação */ public ContatosCursor RetornarContatos(ContatosCursor.OrdenarPor ordenarPor) { String sql = ContatosCursor.CONSULTA + (ordenarPor == ContatosCursor.OrdenarPor.NomeCrescente ? "ASC" : "DESC"); SQLiteDatabase bd = getReadableDatabase(); ContatosCursor cc = (ContatosCursor) bd.rawQueryWithFactory(new ContatosCursor.Factory(), sql, null, null); cc.moveToFirst(); return cc; } public static class ContatosCursor extends SQLiteCursor { public static enum OrdenarPor{ NomeCrescente, NomeDecrescente } private static final String CONSULTA = "SELECT Contatos.ID, Nome, Endereco FROM Contatos ORDER BY Nome "; private ContatosCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { super(db, driver, editTable, query); } private static class Factory implements SQLiteDatabase.CursorFactory { @Override public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { return new ContatosCursor(db, driver, editTable, query); } } public long getID() { return getLong(getColumnIndexOrThrow("Contatos.ID")); } public String getNome() { return getString(getColumnIndexOrThrow("Nome")); } public String getEndereco() { return getString(getColumnIndexOrThrow("Endereco")); } } |
Como o foco deste post não é a criação de um layout ideal para exibição de dados, criei um bem simples que utiliza um TextView para mostrar os dados do banco concatenando strings. O código XML abaixo mostra como deve ser codificado o layout main.xml e a imagem mostra como ele deve se parecer:
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 |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/contatos" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:id="@+id/listaContatos" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Nenhum contato cadastrado." /> <Button android:id="@+id/btnNovo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Novo Cadastro" /> </LinearLayout> |
E o código Java a seguir mostra o início da codificação da Activity principal, que mantém o nome padrão de MainActivity.java (este código vai dentro do corpo da classe MainActivity):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Button btnSalvar, btnCancelar, btnNovo; EditText txtNome, txtEndereco, txtTelefone; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CarregarInterfaceListagem(); } public void CarregarInterfaceListagem() { setContentView(R.layout.main); //configurando o botão de criar novo cadastro btnNovo = (Button)findViewById(R.id.btnNovo); btnNovo.setOnClickListener(new OnClickListener(){ public void onClick(View v) { CarregarInterfaceCadastro(); }}); CarregarLista(this); } |
Neste trecho de código podemos ver as chamadas aos métodos CarregarLista (mostrado abaixo) e mais a seguir implementaremos o CarregarInterfaceCadastro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public void CarregarLista(Context c) { ContextoDados db = new ContextoDados(c); ContatosCursor cursor = db.RetornarContatos(ContatosCursor.OrdenarPor.NomeCrescente); for( int i=0; i < cursor.getCount(); i++) { cursor.moveToPosition(i); ImprimirLinha(cursor.getNome(), cursor.getTelefone()); } } public void ImprimirLinha(String nome, String telefone) { TextView tv = (TextView)findViewById(R.id.listaContatos); if(tv.getText().toString().equalsIgnoreCase("Nenhum contato cadastrado.")) tv.setText(""); tv.setText(tv.getText() + "rn" + nome + " - " + telefone); } |
Este código nada mais faz do que concatenar strings em um TextView para simular uma listagem de dados vindos do banco. Com o código que vimos até agora, já é possível listar os dados que já estão no banco, mas provavelmente você irá querer escrever também, que veremos na próxima parte!
Escrevendo no Banco de Dados
Voltando ao ContextoDados, o trecho de código a seguir insere um novo registro no banco de Contatos e serve como exemplo para futuras inserções em outras tabelas (este código vai dentro da classe ContextoDados):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public long InserirContato(String nome, String telefone, String endereco) { SQLiteDatabase db = getReadableDatabase(); try { ContentValues initialValues = new ContentValues(); initialValues.put("Nome", nome); initialValues.put("Telefone", telefone); initialValues.put("Endereco", endereco); return db.insert("Contatos", null, initialValues); } finally { db.close(); } } |
E agora devemos criar o layout para a tela de cadastro, que chamei de cadastro.xml. A seção a seguir mostra o código XML para o layout, e a imagem à direita, como ele deve se parecer se tudo estiver correto:
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 |
<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/lblNome" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="8dp" android:layout_y="9dp" android:text="@string/lblNome" android:textAppearance="?android:attr/textAppearanceSmall" /> <TextView android:id="@+id/lblTelefone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="7dp" android:layout_y="52dp" android:text="@string/lblTelefone" android:textAppearance="?android:attr/textAppearanceSmall" /> <TextView android:id="@+id/lblEndereco" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="8dp" android:layout_y="95dp" android:text="@string/lblEndereco" android:textAppearance="?android:attr/textAppearanceSmall" /> <EditText android:id="@+id/txtNome" android:layout_width="180dp" android:layout_height="40dp" android:layout_x="78dp" android:layout_y="8dp" android:inputType="textPersonName" > <requestFocus /> </EditText> <EditText android:id="@+id/txtTelefone" android:layout_width="180dp" android:layout_height="40dp" android:layout_x="78dp" android:layout_y="50dp" android:inputType="phone" /> <EditText android:id="@+id/txtEndereco" android:layout_width="180dp" android:layout_height="40dp" android:layout_x="78dp" android:layout_y="93dp" android:inputType="textPostalAddress" /> <Button android:id="@+id/btnSalvar" android:layout_width="82dp" android:layout_height="wrap_content" android:layout_x="173dp" android:layout_y="142dp" android:text="@string/btnSalvar" /> <Button android:id="@+id/btnCancelar" android:layout_width="92dp" android:layout_height="wrap_content" android:layout_x="78dp" android:layout_y="141dp" android:text="Cancelar" /> </AbsoluteLayout> |
Note que neste layout temos alguns EditTexts e dois botões. O botão de cancelar apenas retorna à tela inicial, com a listagem tosca de contatos. Já o botão de salvar, deve invocar o método de inserir novo registro, da classe ContextoDados. O código restante da MainActivity está abaixo, para que você possa copi-colar:
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 |
public void CarregarInterfaceCadastro() { setContentView(R.layout.cadastro); //configurando o botão de cancelar cadastro btnCancelar = (Button)findViewById(R.id.btnCancelar); btnCancelar.setOnClickListener(new OnClickListener(){ public void onClick(View v) { CarregarInterfaceListagem(); }}); //configurando o formulário de cadastro txtNome = (EditText)findViewById(R.id.txtNome); txtEndereco = (EditText)findViewById(R.id.txtEndereco); txtTelefone = (EditText)findViewById(R.id.txtTelefone); //configurando o botão de salvar btnSalvar = (Button)findViewById(R.id.btnSalvar); btnSalvar.setOnClickListener(new OnClickListener(){ public void onClick(View v) { SalvarCadastro(); }}); } public void SalvarCadastro() { ContextoDados db = new ContextoDados(this); db.InserirContato(txtNome.getText().toString(), txtTelefone.getText().toString(), txtEndereco.getText().toString()); setContentView(R.layout.main); CarregarLista(this); } |
O código é auto-descritivo, mas caso tenha alguma dúvida, não hesite em perguntar pelos comentários.
Conclusões
Este post mostrou o básico de seleção e inserção de dados, com uma exibição bem tosca, longe de ser a ideal para uma base de dados. Você pode realizar o download dos fontes completos do projeto no link abaixo. Posts futuros devem cobrir outros aspectos da persistência de dados, como deleção e atualização, bem como listagem profissional dos dados usando ListViews personalizadas e dicas de boas práticas com SQLite.
Mas isto fica pra próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.