Transparecendo a persistência de dados em Android com MVC

Propaganda
_sqlite
Transparecendo a
persistência de dados
em Android com MVC
Conhecendo o funcionamento do SQLite e aplicando as práticas
do modelo MVC no desenvolvimento da camada DAO.
Douglas Cavalheiro | [email protected]
Bacharel em Sistemas de Informação, trabalha com Java desde 2009, focado no desenvolvimento Android a partir de 2011. Desenvolveu a biblioteca DroidPersistence para persistência em SQLite na S.O. para dispositivos móveis do Google.
M
uitas aplicações comerciais estão sendo voltadas
à mobilidade. Empresas já vêm migrando parte
de seus módulos para tablets ou celulares, principalmente devido à facilidade de manter esses aparelhos
sempre conectados e à mão. A grande vantagem hoje
de portar aplicações, ou parte delas, para tablets é
todo o ambiente oferecido ao desenvolvedor, mesmo
que limitado a desempenho, mas funcional. É o caso
do Android SDK que nos permite escrever aplicações
na linguagem Java e integrar com o banco de dados
SQLite, lembrando que é possível utilizar outras formas de storage como DB4Object.
O conceito MVC se aplica muito bem em projetos
deste porte, pois nos permite organizar e distribuir parte das camadas para demais projetos ou envolver na
nova aplicação outros projetos já amadurecidos que estejam nesta estrutura (Modelo, Visão e Controle). Aplicaremos este conceito de forma transparente e agradável para manutenções claras e com resultados bem
consolidados ao longo do tempo.
Temos anseio de iniciar o mais breve possível a codificação quando se trata de novas tecnologias como o
Android, mas o resultado fica ainda melhor quando programar não é a preocupação principal e sim o objetivo a
ser alcançado da forma mais simples possível. Planejar
uma aplicação móvel que terá constante interação com
base de dados torna-se uma atividade complexa se não
houver métricas de desenvolvimento, assim como em
qualquer outro sistema, é importante resgatarmos todas as boas práticas que vamos adquirindo ao longo do
tempo e adequá-las quando formos ingressar para um
novo ambiente. Olhando pelo lado do desenvolvedor, a
garantia de um bom resultado no novo projeto é o sucesso das aplicações desenvolvidas anteriormente. Portanto, antes de iniciar o projeto, analise sua aplicação
numa linha que inicie do ambiente real com regras de
negócio e termine na implementação, esta observação
parece filosófica, mas sugere uma visão menos técnica
e voltada ao mundo real, tendo por consequência as expectativas do interessado (usuário) sendo atendidas da
melhor forma possível pelo analista.
Definindo o Projeto
Seguindo a arquitetura MVC, além da view, a camada de “Business Object” ou “Service” irá realizar
validações e repassar para os objetos DAO as funções
de CRUD. A layer DAO terá uma estrutura com interfaces que declaram os métodos que serão implementados por cada DAO, isto permite flexibilidade para
projetos não Android e homogenização da interação
de dados. Também, é claro, o modelo que será nossa classe Cliente. Em qualquer dúvida, o projeto de
exemplo está disponível no site da MundoJ.
DAO
Uma ótima prática para utilização de DAOs é a
criação de uma interface que declara os métodos que
irão ser utilizados. O motivo? O reúso. O objeto DAO
que implementar esta interface poderá ter implementação diferente, mas as assinaturas dos métodos
serão as mesmas. Conforme o que é mostrado por
Silveira[2], programar voltado à interface e não a implementação é uma das boas práticas de orientação a
objetos. Um exemplo numa aplicação Java é criar os
métodos na interface, podemos codificar uma classe DAO que utiliza Connection e outra que utiliza o
EntityManager da JPA que implementam a interface.
São implementações diferentes, mas o objetivo das
funções é retornar o mesmo resultado. Além do reúso, usar interfaces explica a linha de partir do menos
33 \
Quem já está acostumado a desenvolver em Java com certeza já se
deparou com atividades de acesso aos dados e usou JDBC com suas
factories (Connection ou implementações JPA, por exemplo) para
desenvolver os famosos DAOs. No sistema Android, não temos o
mesmo fluxo oferecido nas aplicações Java web ou desktop, mas podemos adequar boas práticas e minimizar o código para a camada
de persistência e consulta de dados.
técnico para o mais abrangente (implementação)
durante o desenvolvimento para estar mais próximo
do negócio neste início de construção.
Listagem 1. Criação da Interface DAO.
sobrescritos.
Listagem 2. Criação da Classe Dao.
public class ClienteDao extends SQLiteOpenHelper
implements IClienteDao{
package br.com.cadastroclientes.dao;
private SQLiteDatabase database;
import java.util.List;
/**Construtor*/
public ClienteDao(Context context, String name,
CursorFactory factory,int version) {
import br.com.cadastroclientes.model.Cliente;
public interface IClienteDao {
}
Cliente getByNome(String nome);
Cliente getById(long id);
List<Cliente> listAll();
boolean saveCliente(Cliente cliente);
boolean deleteCliente(long id);
boolean updateCliente(Cliente cliente);
O código da Listagem 1 deixa claro que não há
dependência nenhuma de bibliotecas ou outras classes fora do contexto do projeto. Para projetos multiplataforma, a interface DAO, a camada modelo e
a camada de negócio são candidatas a um projeto
separado. Imagine um projeto Web e Android que
ambos devem cadastrar clientes, não fica prático copiar e colar o código de um projeto a outro. Numa
simples manutenção no modelo, este deverá ser alterado em ambos os projetos, não queremos isso, por
fim criamos um projeto “core” que mantém toda esta
estrutura e adicionamos este projeto ao build path
do projeto mobile e no projeto web.
Implementando o DAO
Aqui se inicia a implementação da interface
usando as particularidades do SDK Android. Num
DAO Android a classe deve, além de implementar a
interface, estender SqliteOpenHelper, pois esta classe nos permite interagir com o objeto Database, do
qual provém as ações CRUD. Consequentemente o
SqliteOpenHelper traz alguns métodos para serem
/ 34
super(context, name, factory, version);
}
database = getWritableDatabase();
}
//demais métodos
No Construtor, encontramos parâmetros que devem ser mantidos, tais como Context, que é o contexto da aplicação, sendo obtido nas classes de Activity
e Application do Android. O nome do banco de dados, o CursorFactory (este, é um criador de query, no
caso da aplicação de exemplo não será usado, sendo
passado null) e a versão do banco de dados, esta versão é importante caso queira realizar um novo build
da aplicação com alterações de estrutura ou inserção
de dados, a criação de um novo campo na tabela, por
exemplo. Logo abaixo temos o objeto privado SQLiteDatabase sendo instanciado pelo método getWritableDatabase() este método abre o banco de dados
para uso e o cria se necessário.
Como é criado o banco de dados da aplicação?
E como criamos as estruturas mencionadas anteriormente? A classe SQLiteOpenHelper nos dá uma
visão melhor de como a mágica acontece com os métodos de OnCreate e onUpgrade (Listagem 3).
Listagem 3. Métodos que criam a tabela e atualizam
se necessário.
/**Método chamado quando o banco de dados é criado*/
@Override
public void onCreate(SQLiteDatabase db) {
StringBuilder sb = new StringBuilder();
sb.append(“create table if not exists CLIENTE(“);
sb.append(“ID integer not null primary key
autoincrement,”);
sb.append(“NOME varchar(50),”);
sb.append(“SOBRENOME varchar(50),”);
sb.append(“IDADE integer”);
sb.append(“);”);
}
db.execSQL(sb.toString());
/**Método lançado caso a versão do banco de dados tenha
sido atualizada
* na mesma aplicação. Usado geralmente para adicionar
novos campos
* ou “Drop” da tabela e criar a nova chamando o
onCreate*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
if(newVersion > oldVersion){
/*inserir aqui scripts de drop ou alter table
se necessário de acordo com a versão do banco
de dados */
}
}
//demais métodos
Cliente cliente = new Cliente();
cliente.setId(cursor.getInt(0));
cliente.setNome(cursor.getString(1));
cliente.setSobrenome(cursor.getString(2));
cliente.setIdade(cursor.getInt(3));
}
return cliente;
/**Instancia o objeto ContentValues, facilitando insert e
update*/
private ContentValues serializeContentValues(Cliente
cliente){
ContentValues values = new ContentValues();
values.put(“NOME”, cliente.getNome());
values.put(“SOBRENOME”, cliente.getSobrenome());
values.put(“IDADE”, cliente.getIdade());
return values;
}
//demais métodos
Cursor é uma interface que funciona como o ResultSet do java.sql onde obtemos o resultado de uma
consulta e podemos interagir entre ela, a prática de
construção de um método como o executeSelect facilita consultas onde o desenvolvedor tem de trazer
todo o objeto da tabela e para transformar os dados
em objeto. Outro método como o serializeByCursor é
recomendado para economia e versatilidade no código do DAO transformando cada resultado em um objeto instanciado, já o método serializeContentValues
faz o contrário, transforma o objeto em values para
insert ou update.
Com o código apresentado na Listagem 4 podemos fazer qualquer operação de dados. No DAO, basta
implementar os métodos da interface descrita anteriormente.
No método onCreate encontramos algo familiar
que é a instrução de “CREATE TABLE” definindo então a estrutura da tabela cliente. O SqliteDatabase
usado como parâmetro é o banco de dados aberto na
construção do banco de dados. Lembrando que não
é necessário invocar o método onCreate, pois ele é
disparado no momento que o método getWritableDatabase é chamado. Na função onUpgrade informamos
instruções de alter table ou inserts e updates no caso
Listagem 5. Implementação da interface no objeto
de sua versão do banco de dados ter sido alterada
DAO.
como dito anteriormente.
Listagem 4. Métodos que auxiliam na interação
entre o objeto e os dados.
/**Método que rertorna um cursor de um select simples,
* mais amigável, se necessário incluir os parametros de
* groupBy e having*/
private Cursor executeSelect(String selection, String[]
selectionArgs, String orderBy){
return database.query(“CLIENTE”, new String[]{
“ID”,
“NOME”,
“SOBRENOME”,
“IDADE”
}, selection, selectionArgs, null, null, orderBy);
}
/**Método que instancia o objeto e o serializa
* com o resultado de um Cursor*/
private Cliente serializeByCursor(Cursor cursor){
@Override
public Cliente getByNome(String nome) {
Cliente cliente = null;
Cursor cursor = executeSelect(
“NOME = ?”,
new String[]{nome},
null);
if(cursor != null && cursor.moveToFirst()){
cliente = serializeByCursor(cursor);
}
}
if(! cursor.isClosed())
cursor.close();
return cliente;
35 \
@Override
public List<Cliente> listAll() {
List<Cliente> list = new ArrayList<Cliente>();
Cursor cursor = executeSelect(
null,
null,
“1”);
if(cursor != null && cursor.moveToFirst()){
do{
list.add(serializeByCursor(cursor));
}while(cursor.moveToNext());
}
}
if(! cursor.isClosed())
cursor.close();
return list;
@Override
public boolean saveCliente(Cliente cliente) {
ContentValues values = serializeContentValues(cli
ente);
//retorno do insert é a quantidade de registros
inseridos
if(database.insert(“CLIENTE”, null, values) > 0)
return true;
else
return false;
}
@Override
public boolean deleteCliente(long id) {
if(database.delete(
“CLIENTE”,
“ID = ?”,
new String[]{
String.valueOf(id)
}) > 0)
return true;
else
return false;
}
//demais métodos de busca, listagem e update
Business Object (service) e seu baixo
acoplamento
Business Object ou Service é um objeto voltado
ao controle do negócio, como cita Fowler[3] sobre
Design guiado pelo domínio. Neste objeto colocamos
regras de negócio antes de persistir ou realizar alguma tarefa na camada visão. Validações também são
bem-vindas.
No objeto ClienteBO é necessário invocar os métodos do ClienteDao, mas necessitando o mínimo
possível do objeto ClienteDao, faremos isso usando
a interface DAO.
/ 36
//trecho
public class ClienteBO {
private IClienteDao clienteDao;
public ClienteBO(Context context,
String databaseName,
int databaseVersion){
clienteDao = new ClienteDao(
context,
databaseName,
null,
databaseVersion);
}
public void saveCliente(Cliente cliente) throws
Exception{
validateCadastroCliente(cliente);
if(! clienteDao.saveCliente(cliente))
throw new Exception(“Cliente não pode ser
salvo !”);
}
public List<Cliente> getListCliente(){
return clienteDao.listAll();
}
// demais métodos usando o dao e o
// validateCadastroCliente omitidos
}
O atributo clienteDao utilizado em todos os métodos de interação é a interface instanciada no construtor com sua implementação Android ClienteDao,
neste procedimento mantemos a pouca dependência
do objeto que implementa a interface. Mas e o construtor? Este tem particularidades do Android usando
Dependence Injection, este BO não poderá ser usado
como uma lib ou projeto avulso. Isso é verdade, mas
a solução é simples: criar um objeto BO para o android etendendo do BO genérico e só implementando
o construtor já resolveria o problema.
Usando a Classe Application
Um recurso muito útil para desenvolvedores Android é poder sobrescrever a classe Application. Sua
função é instanciar objetos globais que podem ser
usados na aplicação. Conforme a documentação[4],
singletons podem resolver isso, porém, se o singleton precisar de um Context ou demais recursos como
Broadcast Receivers, já seria mais funcional utilizar
a própria classe Application que contém estes atributos.
Listagem 6. Classe Application.
public class CadastroClientesApp extends Application{
private ClienteBO clienteBO;
@Override
public void onCreate() {
super.onCreate();
clienteBO = new ClienteBO( this, “DATABASE_
CLIENTES”, 1 );
}
}
A fim de que o SDK entenda que a classe escrita
será a que implementará a aplicação, é necessário informar o AndroidManifest.xml para que ela seja executada nas operações de início e fim. Desta maneira, a
tag android:name deve conter o nome da classe: <application android:name=”CadastroClientesApp” …
Usando o BO instanciado na Activity
Com a classe Application executando, e a Classe
BO instanciada, torna-se fácil a chamada do objeto e
seus métodos, desde que também se utilize o método getApplication() para buscar a instância do objeto
Application dentro da activity Android:
//trecho
private CadastroClientesApp app;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.cadastro_cliente);
//Instancia a aplicação
app = (CadastroClientesApp) getApplication();
}
E, por fim, é possível desfrutar dos BO chamando-o diretamente da Application:
//Trecho
/* demais métodos omitidos*/
app.getClienteBO().saveCliente(cliente);
/* demais métodos omitidos*/
A grande vantagem de usar um único Business
Object instanciado é a minimização de locks no banco de dados, pois não haverá preocupação em abrir
e fechar transações durante o runtime da aplicação,
mas se precisar abrir e fechar as transações manualmente por questões de otimização ou até mesmo
controle próprio, os métodos beginTransaction e endTransaction do SQLiteDatabase. Outro benefício é o
ganho em heap, mas é interessante monitorá-lo para
ver o que pode estar errado numa possível lentidão
na inicialização.
É importante imaginar que se a utilização de um
BO for somente uma vez e chamada pelo usuário esporadicamente, talvez não seja necessária a criação
do objeto na classe Application durante a inicialização do sistema e sim dentro da própria activity, porém, um cuidado especial ao parâmetro version no
construtor do DAO, pois se muitas criações do BO
ficarem espalhadas pela aplicação e um upgrade na
versão do BD for necessário, o refactoring poderá ser
doloroso. Um meio de resolver o parâmetro version
é usando uma classe com constantes ou no modelo
singleton.
Considerações Finais
O modo em que se aplica um banco de dados Sqlite no projeto Android fica muito a cargo do desenvolvedor. Fazer a leitura e atualização de dados pode ser
uma prática problemática durante o desenvolvimen-
to ou manutenção posterior. O artigo sugere formas
de aproveitar bem os conceitos de desenvolvimento
orientado a objetos e grandes facilidades que o Android SDK proporciona. Algumas particularidades do
Android podem trazer benefícios à aplicação como a
classe “Application” que é instanciada no momento
que a aplicação é executada (não a confunda com
Singleton), e esta pode conter classes provedoras de
negócio que servem de ponte para o acesso a todos
os DAOs do projeto, sendo fácil e rápido chamar os
métodos sem ter que ficar instanciando a cada necessidade. Tudo dependerá de como for projetada a
aplicação, quantas serão as interações entre os dados, quantas tabelas existirão no banco de dados etc.
Uma pergunta a ser feita durante o escopo do projeto
é: realmente é necessário usar DAO, BO e criar uma
classe gerenciadora de dados para somente usar a tabela “Usuário” na tela de login? A resposta é implícita
se tudo estiver projetado. De qualquer modo o artigo
tem foco nas aplicações comerciais, que exigem bastante carga e interação de dados ou outra aplicação
do gênero. Para jogos, aplicações como redes sociais,
talvez o Sqlite nem seja a melhor recomendação, o
Android tem também outras formas de gravar informações como o Shared Preferences e escrita em arquivo.
/referências
> http://www.sqlite.org/whentouse.html
> Introdução à Arquitetura e Design de Software – Uma
Visão sobre a Plataforma Java – Editora: Elsevier – Campus
> http://martinfowler.com/bliki/AnemicDomainModel.html
> http://developer.android.com/reference/android/app/
Application.html
/para saber mais
> Existem frameworks que auxiliam na escrita do DAO, sendo
possível diminuir seu código, principalmente na criação da
tabela, métodos de busca e inserção, exclusão e atualização
de dados. O DroidPersistence é um projeto que possibilita
isso. Mas não descarte o fato de ao menos dominar como
trabalha o SDK antes de usar qualquer framework, essa ideia
ajudará a resolver possíveis problemas e também servirá de
argumento para decisões técnicas de projeto.
> O SQLite é um banco de dados relacional simples e a
linguagem usada é o SQL (como seu próprio nome indica).
Uma das limitações encontradas logo de início em relação a
outros SGBDs (Sistemas Gerenciadores de Banco de Dados)
é a falta de Stored Procedures, a própria documentação
SQLite[1] explica que por ser “Lite” não tem este recurso
e se o desenvolvedor necessita utilizar Stored Procedures
em seu BD, procure um que lhe atenda, como o Oracle ou
PostgreSql. Contudo, as operações de CRUD (Create, Read,
Update e Delete – Criar, Ler, Atualizar e Excluir) são feitas
normalmente.
37 \
Download