Persistência de Dados BlackBerry – SQLite e Objetos Persistentes Já tive a oportunidade de escrever sobre persistência de dados para quase todas as plataformas, porém, ainda estava em dívida com uma delas, o filho da canadense RIM, o BlackBerry. Sendo assim, resolvi pagar esta dívida. Vamos criar um projeto de exemplo, um cadastro muitos simples de amigos programadores BlackBerry, nas diferentes seções do texto vamos persistir os dados de maneiras distintas, opções oferecidas pela API. Mãos a obra:
O Projeto O foco deste artigo/tutorial é claramente a persistência de dados, logo, não vamos nos preocupar com o look and feel do usuário. A primeira tela poderá ter duas aparências: • No caso de não existirem desenvolvedores cadastrados, o usuário será informado através de uma mensagem na tela (Figura 1(a)). • No caso de existirem desenvolvedores cadastrados, uma listagem dos mesmos será apresentada ao usuário (Figura 1(b));
Figura 1(a)
Figura 1(b)
A home também terá um menu personalizado, com uma opção Inserir. Ao escolher esta opção, o usuário é direcionado a tela de inserção. Também muito simples no quesito layout. Veja a Figura 2:
Figura 2: Tela de Inserção.
Codificação das Telas O primeiro passo é criar o projeto. Uma sugestão de nome é CadastroDevBB. Depois disso, podemos criar a classe que atuará como o controller do modelo MVC (Model-View-Controller). Com o aumento da complexidade das aplicações desenvolvidas, tornase relevante a separação entre os dados e a apresentação das aplicações. Desta forma, alterações feitas no layout não afetam a manipulação de dados, e estes poderão ser reorganizados sem alterar o layout. Esse padrão resolve este problema através da separação das tarefas de acesso aos dados e lógica de negócio, lógica de apresentação e de interação com o utilizador, introduzindo um componente entre os dois, o controlador.
Veja a Listagem 1 com o código da classe Main: Listagem 1: Classe Main 1:package controller; 2: 3:import view.TelaHome; 4:import view.TelaInsEdit; 5:import net.rim.device.api.ui.UiApplication; 6: 7:public class Main extends UiApplication { 8: private TelaHome telaHome; 9: private TelaInsEdit telaInsEdit; 10: 11: public static void main(String[] args) { 12: new Main().enterEventDispatcher(); 13: } 14:
15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33:}
public Main() { telaHome = new TelaHome(this); telaInsEdit = new TelaInsEdit(this); pushScreen(telaHome); } public void telaInsEdit(){ pushScreen(telaInsEdit); } public void inserir(String nome, String email){ telaHome.insert(nome, email); popScreen(telaInsEdit); } public void cancelaInserir(){ popScreen(telaInsEdit); }
A classe Main herda diretamente de UiApplication. Esta, por sua vez, deve ser usada em qualquer aplicação que contenha uma interface gráfica. Caso contrário, a classe Application pode ser usada. Na linha 12, informamos que esta classe responderá aos eventos informados pelo sistema operacional. Perceba também que criamos duas instâncias para as futuras telas da aplicação. Na linha 8 temos o objeto TelaHome, sua instância é criada na linha 16. Nas linhas subseqüentes a essas, temos o objetos e instância de TelaInsEdit. A BlackBerry API trabalha com uma pilha de telas, sendo que aquela que está no topo, é mostrada ao usuário. Como conseqüência, temos os métodos pushScreen e popScreen na classe UiApplication. Na linha 18 colocamos a tela inicial nesta pilha. Já criamos três métodos auxiliares, que gerenciarão a troca entre telas e o comando de inserir dados no repositório. O telaInsEdit, na linha 21, apenas mostra ao usuário a tela de inserção de dados. O inserir, iniciando na linha 25, chamará o método homônimo da classe TelaHome e, depois disso, retira a tela de novo contato da pilha, fazendo com o usuário volte a visualizar a tela inicial com o novo contato. O cancelaInserir é chamado pela TelaInsEdit, quando o usuário desiste de adicionar um contato. Agora vamos a classe TelaHome: Listagem 2: Classe TelaHome 1:package view; 2:import … 3: 4:public class TelaHome extends MainScreen { 5: private Main controller; 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:}
public TelaHome(Main main) { this.controller = main; criarBD(); } public void criarBD() { … } public void mostraMsg(final String msg) { UiApplication.getUiApplication().invokeLater(new Runnable() { public void run() { Dialog.alert(msg); } }); } public void insert(String nome, String email) {} public void lerPessoas() {} public void deletar(Row r){} class InsertMI extends MenuItem { public InsertMI() { super("Inserir", 10, 20); } public void run() { controller.telaInsEdit(); } } protected void makeMenu(Menu menu, int instance) { super.makeMenu(menu, instance); menu.add(new InsertMI()); }
A classe não apresenta nenhum segredo. O construtor recebe uma instância da classe Main (linha 8). Criamos um item de menu na linha 28, direcionando sua ação para o método telaInsEdit do controller, que apenas mostra a tela de inserção de dado. Por fim, na linha 38, sobrescrevemos o método makeMenu da classe MainScreen, super classe de TelaHome, para adicionar a opção Inserir. Perceba que a classe apresenta métodos para todas as operações relacionadas a um aplicativo que trabalha com persistência de dados. São eles: insert, lerPessoas, deletar e criarBD. Todos eles ainda não estão implementados. Será a parte importante deste trabalho. E a classe TelaInsEdit? Veja a Listagem 3: Listagem 3: Classe TelaInsEdit
1:package view; 2:import … 3: 4:public class TelaInsEdit extends MainScreen { 5: private EditField edtNome; 6: private EditField edtEmail;
7: 8:
private Main controller;
9: public TelaInsEdit(Main main){ 10: this.controller = main; 11: edtNome = new EditField("Nome: ", ""); 12: edtEmail = new EditField("E-Mail: ", ""); 13: 14: add(edtNome); 15: add(edtEmail); 16: } 17: 18: class InsertMI extends MenuItem { 19: public InsertMI() { 20: super("Inserir", 10, 20); 21: } 22: 23: public void run() { 24: controller.inserir(edtNome.getText(), edtEmail.getText()); 25: } 26: } 27: 28: class CancelaMI extends MenuItem { 29: public CancelaMI() { 30: super("Cancelar", 10, 20); 31: } 32: 33: public void run() { 34: edtNome.setText(""); 35: edtEmail.setText(""); 36: controller.cancelaInserir(); 37: } 38: } 39: 40: protected void makeMenu(Menu menu, int instance) { 41: super.makeMenu(menu, instance); 42: menu.add(new InsertMI()); 43: menu.add(new CancelaMI()); 44: } 45:}
Classe que também não contém nenhum detalhe muito importante. Preste atenção apenas no método run da classe InsertMI. Ela aciona o método inserir do controlador, a classe Main, passando por parâmetro o conteúdo dos campos de textos.
Persistindo Informações - SQLite A partir de agora nosso trabalho será exclusivamente na classe TelaHome. Nosso primeiro método de persistência de dados a ser estudado será o SQLite, presente na versão 5.0 e superior do BlackBerry OS. Vamos começar com o método onde criamos nosso banco de dados. Lembram do método criarBD, visto na listagem 2, que estava vazio? Bem, substitua o mesmo pela implementação mostrada na Listagem 4:
Listagem 4: Método criarBD
1:public void criarBD() { 2: try { 3: URI myURI = URI.create("file:///SDCard/databases/CadastroDev/CadastroDev"); 4: if (DatabaseFactory.exists(myURI)) { 5: sqliteDB = DatabaseFactory.open(myURI); 6: lerPessoas(); 7: } else { 8: sqliteDB = DatabaseFactory.create(myURI); 9: Statement st = sqliteDB.createStatement("CREATE TABLE Pessoa ( Nome TEXT, Email TEXT )"); 10: st.prepare(); 11: st.execute(); 12: st.close(); 13: } 14: 15: mostraMsg("Processo de abertura/criação de banco realizado com sucesso."); 16: } catch (MalformedURIException e1) { 17: mostraMsg("Criar Banco: MalformedURIException"); 18: } catch (IllegalArgumentException e1) { 19: mostraMsg("Criar Banco: IllegalArgumentException"); 20: } catch (DatabasePathException e1) { 21: mostraMsg("Criar Banco: DatabasePathException"); 22: } catch (DatabaseIOException e1) { 23: mostraMsg("Criar Banco: DatabaseIOException"); 24: } catch (DatabaseException e1) { 25: mostraMsg("Criar Banco: DatabaseException"); 26: } 27:}
Até a terceira linha ainda não vimos nada de novo, apenas criamos uma instância de URI, para indicar o caminho do nosso banco de dados. Na linha 4 estamos usando a classe DatabaseFactory, que pode ser dissecada neste endereço: http://www.blackberry.com/developers/docs/5.0.0api/net/rim/device/api/dat abase/DatabaseFactory.html. Seu método exists retorna um valor boolean informando se o banco de Segundo a documentação da classe, o SDCard é o local mais indicado para criar uma base de dados. Porém, alguns aparelhos também permitem a criação do banco de dados na memória interna.
dados identificado pela URI existe. O método também pode receber uma String.
Se o teste booleano retornar um valor true, significa que já possuímos esta base de dados, logo, só é preciso ter acesso a ela. Sendo assim, na linha 5 chamamos o método open da mesma classe, passando como parâmetro a URI criada no início do processo. O método open tem versão que aceita uma String, além de, versões que recebem dois parâmetros, sendo o último um valor booleano, definindo se a base de dados será apenas leitura ou também escrita. Depois de abrir o banco de dados chamamos o método lerPessoas. Logo o explicaremos em detalhes. Caso a base de dados ainda não existe, vamos criá-la. Este processo é feito na linha 8, através do método create. Este, por sua vez, é muito parecido com o open. Ambos possuem o mesmo conjunto de versões de construtor, com os mesmos parâmetros e, os dois retornam uma instância da classe Database. Se o banco ainda não existe e acabou de ser criado, também precisamos criar nossa tabela Pessoa. Veja que a String passada para o Seguindo o padrão MVC o ideal seria termos uma classe especializada no tratamento com banco de dados. Como lição de casa, separe todos métodos de TelaHome que interferem na persistência de dados, criando uma nova classe em um pacote chamado model.
método createStatement é uma chamada SQL. Para saber mais sobre o SQLite visite a página oficial: http://www.sqlite.org/. O createStatement retorna uma instância de Statement. Com esta classe em mãos podemos chamar o conjunto de método prepare, execute e close. Eles preparam o statement, processam o mesmo e, por fim, fecham-no e liberam todos os recursos alocados; respectivamente.
Se o método criarBD verificar que o banco de dados já existe, o mesmo é aberto e o método lerPessoas é chamado. Vamos dar uma olhada na Listagem 5: Listagem 5: Método lerPessoas 1:public void lerPessoas() {
2: try { 3: deleteAll(); 4: Statement st = sqliteDB.createStatement("select Nome, Email from Pessoa"); 5: st.prepare(); 6: Cursor c = st.getCursor(); 7: 8: int i = 0; 9: while (c.next()) { 10: final Row r = c.getRow(); 11: i++; 12: HorizontalFieldManager hor = new HorizontalFieldManager(Manager.HORIZONTAL_SCROLLBAR| Manager.HORIZONTAL_SCROLL); 13: hor.add(new LabelField("Nome: "+r.getString(0)+". Email: "+r.getString(1), FOCUSABLE | Field.FIELD_VCENTER)); 14: hor.add(new ButtonField("Deletar"){ 15: protected boolean navigationClick(int status, int time) { 16: deletar(r); 17: return true; 18: }; 19: }); 20: add(hor); 21: } 22: 23: if (i == 0) { 24: add(new RichTextField("Sem desenvolvedores cadastrados.")); 25: } 26: st.close(); 27: } catch (DataTypeException e) { 28: mostraMsg("Erro ao dar Select: DataTypeException."); 29: } catch (DatabaseException e) { 30: mostraMsg("Erro ao dar Select: DatabaseException."+ e.getMessage()); 31: } 32:}
Primeiramente, o método remove todos os componentes de UI que estão presentes na tela (linha 3). Na seqüência cria um novo Statement (linha 4) com o SQL select, preparando-o na linha seguinte. A linha 6 utiliza o método getCursor para recuperar a instância de Cursor, que funciona como um apontador para os dados recuperados na consulta SQL. Veja a Figura abaixo:
O Cursor pode ser imaginado como a seta localizada no lado direito. Para que possamos iterar sobre os registros, a classe fornece alguns métodos muito propícios para um “ponteiro”: first, last, next, prev e position (int indice). Na linha 9 criamos um laço, tendo como verificação de parada é o método next, este, por sua vez, retorna um valor booleano true, se existem mais registros a serem lidos ou, false, caso contrário. Na linha 10 recuperamos a linha, representada pela classe Row, onde o cursor está. A classe Row possui métodos get para vários tipos primitivos: • getLong • getShort • getString • getInteger • getFloat • getDouble • getBoolean • getByte Além disso, possui outro método interessante. O getColumnNames, que retorna um vetor de String com os nomes das colunas. Voltando ao código. Na linha 13, recuperamos as Strings das colunas localizadas nos índices 0 e 1, respectivamente. Jogando-as na instância do objeto LabelField, que é visualizado pelo usuário. Além disso, logo abaixo, também criamos o ButtonField deletar. Este, ao ser clicado, chama o método deletar (linha 16). Já que falamos nele, vamos dar uma olhada no método deletar: Listagem 6: Método deletar public void deletar(Row r){
try { Statement stDel = sqliteDB.createStatement("delete from Pessoa where Nome = '"+r.getString(0)+"' and Email = '" + r.getString(1) + "'"); stDel.prepare(); stDel.execute(); stDel.close(); lerPessoas(); } catch (DataTypeException e) { mostraMsg("Erro ao dar delete: DataTypeException."); } catch (DatabaseException e) { mostraMsg("Erro ao dar delete: DatabaseException. "+e.getMessage()); } }
Para saber mais sobre a classe Row: http://www.blackberry.com/developers/docs/6.0.0api/net/rim/device/api/database/Row.html ; Para saber mais sobre a interface Cursor: http://www.blackberry.com/developers/docs/6.0.0api/net/rim/device/api/database/Cursor.html ; Para saber mais sobre a interface Database: http://www.blackberry.com/developers/docs/6.0.0api/net/rim/device/api/ database/Database.html
Nesta altura o leitor já entende perfeitamente a listagem de código. Mas preste atenção em um fato. Se a exclusão for realizada com sucesso o método lerPessoas é chamado novamente, atualizando a tela principal. Faltou apenas dar uma olhada no método insert: Listagem 7: Método insert
public void insert(String nome, String email) { try { Statement st = sqliteDB.createStatement("insert into Pessoa (Nome,Email) values ('" + nome + "', '" + email + "')"); st.prepare(); st.execute(); st.close(); lerPessoas(); } catch (DatabaseException e) { mostraMsg("Erro ao dar Select: DatabaseException."); } }
O leitor não está sentindo falta de algo? Não criamos o método atualizar. Bem, esta função fica como lição de casa.
Persistindo Informações - PersistentStore A classe PersistentStore (http://www.blackberry.com/developers/docs/4.0.2api/net/rim/device/api/sy stem/PersistentStore.html) serve para persistir objetos. Talvez para persistência de grande quantidade de dados, ou até mesmo da complexidade dos mesmos, o SQLite seja a melhor opção. Porém, em alguns casos o PersisntStore agüenta a parada. Até mesmo neste nosso exemplo ele é bem útil. Outro ponto positivo é a presença da API desde as primeiras versões do sistema operacional do BlackBerry. Lembrando, o SQLite está disponível só a partir da versão 5.0. Vamos recriar os mesmos métodos de antes, começando pelo criarBD: Listagem 8: Método criarBD
1:public void criarBD() { 2: //aplicativo exemplo de persistentstore 3: store= PersistentStore.getPersistentObject(0x8ec0eb7e9b1c8744L); 4: if (store.getContents() == null) { 5: store.setContents(new Hashtable()); 6: store.commit(); 7: } 8: 9: _data = (Hashtable) store.getContents(); 10: lerPessoas(); 11:}
Logo na segunda linha utilizamos a classe PersistentStore para criar uma instância de PersistentObject (http://www.blackberry.com/developers/docs/4.0.2api/net/rim/device/api/sy stem/PersistentObject.html). Esta classe encapsula o objeto que será persistido no aparelho. O parâmetro passado para o método getPersistentObject deve ser único por aplicativo. Para conseguir isto, basta escrever um texto qualquer, marcá-lo e clicar com o botão direito do mouse. Se estiver usando Eclipse verá a opção Convert String to Long:
Para constar, a PersistentStore também fornece o método destroyPersistentObject. Na linha seguinte (4) verificamos se o objeto de persistência está vazio, caso afirmativo, configuramos o conteúdo do objeto e acionamos o método commit (linha 6) para efetuar a transação. Na linha 9 recuperamos o objeto persistido na PersistentObject para a variável global _data. Insira a declaração destes dois objetos em sua classe TelaHome: private static Hashtable _data = new Hashtable(); private static PersistentObject store;
Finalizando o método, chamamos o lerPessoas, que também sofreu alterações. Veja a listagem abaixo:
Listagem 9: Método lerPessoas
1:deleteAll(); 2:_data = (Hashtable) store.getContents(); 3:if (!_data.isEmpty()) { 4: Enumeration enum = _data.elements(); 5: 6: while (enum.hasMoreElements()) { 7: final StoreInfo info = (StoreInfo) enum.nextElement(); 8: HorizontalFieldManager hor = new HorizontalFieldManager( Manager.HORIZONTAL_SCROLLBAR | Manager.HORIZONTAL_SCROLL); 9: hor.add(new LabelField("Nome: " + info.getElement(StoreInfo.NOME) + ". Email: " + info.getElement(StoreInfo.EMAIL), FOCUSABLE | Field.FIELD_VCENTER)); 10: hor.add(new ButtonField("Deletar") { 11: protected boolean navigationClick(int status, int time) { 12: deletar(info); 13: return true; 14: }; 15: }); 16: add(hor); 17: } 18:} else { 19: add(new RichTextField("Sem desenvolvedores cadastrados.")); 20:}
Na primeira linha removemos todos os componentes de interface que já estão visíveis, evitando sobreposições. Na linha 2 recuperamos o objeto Hashtable que está sendo persistido. Na linha 3 podemos ver um teste booleano, verificando se o _data está vazio ou não. Caso esteja o processo pula para a linha 19, inserindo uma instância de LabelField na UI. Caso contrário, o processo passa para a linha 4. A linha 4 recupera um Enumeration de _data, utilizando o método elements(). Na linha 6 iniciamos um laço, utilizando como verificador de parada o método hasMoreElements de enum, ou seja, enquanto tivermos elementos, o laço continua a trabalhar. A linha 7 recupera um objeto de enum, uma instância de StoreInfo. Esta classe é mostrada na Listagem 10, que será detalhada a seguir. Por hora, imagine apenas que é uma classe semelhante a um JavaBeans que usamos para organizar a persistência. A linha 8 cria a instância de HorizontalFieldManager. Este gerenciador de layout conterá um LabelField com os dados do desenvolvedor (criado na linha 9( e um botão para remover o registro, criado e adicionado (na linha 10). Perceba na linha 9 que estamos usando um método getElement da classe StoreInfo para recuperar um dado, passando por parâmetro o índice desta informação. A classe StoreInfo, mostrada na Listagem 10, encapsula um objeto Vector (linha 5) e controla os métodos de inserção (linha 17) e recuperação
(linha 13) de dados no mesmo. Porém, o ponto mais importante está na primeira linha: implements Persistable. Qualquer classe que deseja ser persistida deve, obrigatoriamente, implementar esta classe. Listagem 10: Classe StoreInfo
1: class StoreInfo implements Persistable { 2: public static final int NOME = 0; 3: public static final int EMAIL = 1; 4: 5: private Vector _elements; 6: 7: public StoreInfo() { 8: _elements = new Vector(2); 9: _elements.addElement(""); 10: _elements.addElement(""); 11: } 12: 13: public String getElement(int id) { 14: return (String) _elements.elementAt(id); 15: } 16: 17: public void setElement(int id, String value) { 18: _elements.setElementAt(value, id); 19: } 20: }
Agora vamos estudar como ficou o método insert. Listagem 11: Método insert
1: public void insert(String nome, String email) { 2: StoreInfo info = new StoreInfo(); 3: info.setElement(StoreInfo.NOME, nome); 4: info.setElement(StoreInfo.EMAIL, email); 5: _data.put(nome, info); 6: 7: synchronized (store) { 8: store.setContents(_data); 9: store.commit(); 10: } 11: 12: lerPessoas(); 13: }
O método cria uma instância de StoreInfo na primeira linha. Na linha 3 e 4, são configurados os valores de nome e email do desenvolvedores, estes, por sua vez, foram recebidos por parâmetro. Na linha 5 inserimos este novo objeto no Hashtable identificado por _data. Na linha 8 estamos configurando o conteúdo do PersistentObject. Porém, o leitor pode ficar confuso por um motivo: o _data já foi recuperado do objeto de persistência. Exato, estamos inserindo um objeto que já existe, mas, isso não terá efeitos maléficos. Isso porque, a classe
substitui seu conteúdo neste caso. A linha 9 aplica um commit nesta operação. Finalmente, na linha 12, atualizamos a tela do usuário através do método lerPessoas(). Vamos ao método deletar: 1:public void deletar(StoreInfo infos) { 2: 3: _data.remove(infos.getElement(StoreInfo.NOME)); 4: synchronized (store) { 5: store.setContents(_data); 6: store.commit(); 7: } 8: 9: lerPessoas(); 10: }
A única diferença deste método para o anterior está na linha 3. Estamos removendo um elemento da Hashtable, utilizando para isso o método remove da mesma classe e, passando por parâmetro a chave da hash. Esta chave é recuperada utilizando o método getElement de StoreInfo.
Conclusão Neste pequeno tutorial podemos ver a facilidade de persistir dados na BlackBerry API. Tanto o SQLite como a PersistentStore nos fornecem um conjunto de classes, interfaces e métodos completo, tornando este processo indolor para o desenvolvedor. Ambas tem seus pontos positivos e negativos. SQLite é um padrão, utiliza chamadas SQL comum a grande maioria de nós. Porém, seu uso limita-se a versões 5.0 e superiores do BlackBerry OS (logo isso deixará de ser um empecilho). A PersistentStore esta presente em todas as versões do sistema operacional e, pode persistir objetos de forma simples. Poderia ser usada muito bem para persistir os dados de uma tela de configurações por exemplo. Porém, para dados complexos ou em grande número, não é indicada. Cabe a você decidir qual utilizar em seu projeto.
Para saber mais sobre a interface Persistable: http://www.blackberry.com/developers/docs/4.1api/net/rim/device/api/ut il/Persistable.html; Para saber mais sobre a classe PersistentObject: http://www.blackberry.com/developers/docs/4.0.2api/net/rim/device/api/ system/PersistentObject.html; Para saber mais sobre a classe PersistentStore: http://www.blackberry.com/developers/docs/4.0.2api/net/rim/device/api/ system/PersistentStore.html
Sobre Mim Ricardo da Silva Ogliari. Graduado em Ciência da Computação, pósgraduado em Web: Estratégias de Inovação e Tecnologia. Analista de sistemas mobile no Grupo Pontomobi. Autor de dezenas de artigos que foram publicados em anais de congressos nacionais e internacionais, sites especializados e revistas. Palestrante em eventos nacionais e internacionais, como JustJava, Java Day, GeoLivre, ExpoGPS, FISL e FITE, sempre aborda temas relacionados a computação móvel.