LuizTools 2.0

Desde 2010 codificando minhas ideias!

Criando uma app Android com banco de dados SQLite

Post ensinando como utilizar banco de dados SQLite em suas aplicações Android.

Android + SQLite

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, mais conhecido como 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) que provê funcionalidades de SGBD para suas aplicações. Iremos ver algumas dessas funcionalidades neste post, de uma maneira bem simples, mas que servirá como base para estudos futuros.

Armazenamento persistente de dados

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 é necesário compartilhar dados além de suas fronteiras. Android suporta características de um provedor de conteúdo para que as aplicações possam compartilhar dados. Existe uma interface Java para que o Android possa se comunicar com a base de dados relacional SQLite. Esta interface suporta uma implementação de SQL rica o suficiente para qualquer coisa que você queira em uma aplicação mobile, incluindo facilidades como cursores.

Bases de Dados

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, indíces, 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 a biblioteca de 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 atrá à sintaxe SQL propriamente. Um pouco de Engenharia de Software será utilizada, 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):

 

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 no sistema de arquivos */
	private static final String NOME_BD = "Agenda";
	/** A versão da base de dados que esta classe compreende. */
	private static final int VERSAO_BD = 1;
	private static final String LOG_TAG = "Agenda";
	/** Mantém rastreamento do contexto que nós podemos carregar SQL */
	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) 
	{
		Log.w(LOG_TAG, "Atualizando a base de dados da versão " + oldVersion + " para " + newVersion + ", que destruirá todos os dados antigos");
		String[] sql = contexto.getString(R.string.ContextoDados_onUpgrade).split("\n");
		db.beginTransaction();
		
		try 
		{
			ExecutarComandosSQL(db, sql);
			db.setTransactionSuccessful();
		}
		catch (SQLException e) 
		{
			Log.e("Erro ao atualizar as tabelas e testar os dados", e.toString());
			throw e;
		} 
		finally 
		{
			db.endTransaction();
		}
		
		// Isto é apenas didático. Na vida real, você terá de adicionar novas colunas e não apenas recriar o mesmo banco
		onCreate(db);
	}
	
	/**
	* 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 uma interface de abstração dos mesmos. Esta é uma boa prática de programação e deve ser aplicada em todas 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 daaplicaçã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 duas strings estáticas (que ficam no arquivo strings.xml): R.string.ContextoDados_onCreate e R.string.ContextoDados_onUpgrade com os scripts de criação e atualização do banco de dados. Estas strings estão abaixo e não passam de um SQL comum, quevocê já deve estar acostumado:

<string name="ContextoDados_onCreate">
        CREATE TABLE Contatos (ID INTEGER PRIMARY KEY AUTOINCREMENT, Nome TEXT,    Telefone TEXT, Endereco TEXT);
</string>
<string name="ContextoDados_onUpgrade">"
        DROP TABLE IF EXISTS Contatos
</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:

  1. Cria um comando SQL que descreve os dados que você deseja retornar
  2. Executa o comando na base de dados
  3. 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):

 

/** 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"));
		}
	}

Tela de listagem

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:

<?xml version="1.0" encoding="utf-8"?>
<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):

 

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:

 

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() + "\r\n" + 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):

 

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();
		}
	}

Cadastro.xml - Como deve se parecer

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:

<?xml version="1.0" encoding="utf-8"?>
<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:

 

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ê 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 ListAdapters. Mas isto fica pra próxima!

Agenda.zip (68,85 kb)

blog comments powered by Disqus