Olá, hoje vamos falar sobre aplicativos android consumindo serviços. Embora simples, existem alguns caminhos de pedra a seguir se você quiser implementar a solução logo. Vamos mostrar uma pequena aplicação de exemplo de modo a melhor contextualizar a situação.
Para mostrar este exemplo, os pre-requisitos são os seguintes:
- eclipse Java EE com os plugins de glassfish e de android instalados
- android SDK versão 2.2 ou superior
- Oracle java 6 ou openjdk
- glassfish 3.1.1 ou superior como nossa implementação do JEE 6
- mysql 5.1
- Slackware64 13.37
Comecemos pelo ambiente. No Slackware, você deve utilizar o slackpkg para instalar o JDK. Entretanto, antes de usar o slackpkg pela primeira vez, você deve descomentar um e apenas um mirror do arquivo /etc/slackpkg/mirrors. Feito isso, o seguinte comando, executado como root, deverá instalar o JDK da Oracle automaticamente no seu sistema:
slackpkg install jdk
A instalação full do slackware (a melhor opção de instalação, sempre faça essa) já vem com mysql instalado. O que fica manual são os ajustes que devemos fazer.
Primeiro comente a linha 32 do arquivo /etc/rc.d/rc.mysqld. É a linha do --skip-networking, comentando-a as conexões JDBC irão funcionar. Execute em seguida os seguintes comandos:
mysql_install_db --user=mysql
sh /etc/rc.d/rc.mysqld start
E por fim o script interativo que irá fazer perguntas e tornar a instalação "segura":
mysql_secure_installation
Conecte-se ao mysql com o usuário root. Vamos criar o banco para nossa aplicação:
mysql -u root -p
O código a seguir deve ser executado no terminal do mysql que aparece depois de fazer login:
create database mural; grant all privileges on mural.* to mural@localhost identified by 'mural'; flush privileges;
Dê CTRL+D, faça agora login da seguinte maneira:
mysql -p -u mural mural
Entre com a senha criada anteriormente e rode no console o seguinte SQL:
create table mural(id integer not null primary key auto_increment, mensagem varchar(800) not null, data date not null);
O passo seguinte é baixar um eclipse JEE edition da página de downloads do eclipse e descompactar em algum lugar para usar:
Após abrir o programa e escolher um workspace, vá para o menu Help>Eclipse Marketplace... e na caixa de busca do market coloque glassfish e dê enter. Instale o Glassfish Java EE Application Server Plugin for Eclipse, deve ser o primeiro da lista:
Após instalado o plugin, vamos reiniciar o eclipse e enquanto isso baixamos o glassfish e o descompactamos em algum lugar fácil de encontrar. Pegue a versão Full Platform, de preferência a versão .zip, que é a versão não-fique-no-meu-caminho-edition, ;-)
Feche a tela de boas vindas e prossiga para a aba servers; vamos associar o glassfish que você já descompactou (não é?) com este eclipse. Na área vazia da aba servers, botão direito e New>Server>Glassfish>Glassfish 3.1 aperte em Next e a tela pedindo a pasta do glassfish surgirá. Selecione a pasta glassfish de dentro da pasta glassfish3, deve ficar mais ou menos assim:
Feito isto, Next mais uma vez e Finish. Caso você já tenha usado tomcat, o funcionamento do glassfish é bem similar, embora o glassfish tenha "mais super-poderes", por assim dizer. Já podemos criar o projeto do serviço do mural!
Vamos montar um projeto web com suporte a Servlet 3.0 que irá rodar no glassfish. Vá em File>New>Dynamic Web Project. Chame o projeto se mural (sim.. vamos fazer uma pequena aplicação de mural, mas não espere nada muito grandioso.).
Agora fazemos o ponto de entrada da aplicação REST, a classe App herdando de javax.ws.rs.core.Application
package sample.mural; import java.util.Set; import java.util.HashSet; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("/service") public class App extends Application { @Override public Set> getClasses() { Set > classes = new HashSet >(); // TODO ainda vamos criar os Resources return classes; } }
O servidor deverá escanear e publicar sua aplicação automaticamente. Vamos adicionar um Resource. Crie a classe MuralResource:
package sample.mural; import javax.ws.rs.Path; @Path("/mural") public class MuralResource { // TODO acesso ao banco, publicação, etc. }
Retorne à classe App e modifique-a para ficar assim:
package sample.mural; import java.util.Set; import java.util.HashSet;/*TreeSet*/ import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("/service") public class App extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new HashSet<Class<?>>(); classes.add(MuralResource.class); return classes; } }
Agora vem a melhor parte: não precisamos recuperar diretamente o DataSource para fazermos nossas consultas ao banco de dados, tampouco gerenciar manualmente os acessos concorrentes. Tudo o que precisamos fazer é tornar o MuralResource um Enterprise Java Bean, ou EJB para os mais íntimos.
Antigamente, no tempo antes do tempo (2006 pra trás), EJB era motivo de ódio e de ranger de dentes; a especificação JEE era falha e deu inúmeras brechas que permitiram cada grande fornecedor fizesse uma implementação que prendesse a solução em seus próprios produtos, impedindo a promessa da portabilidade dos aplicativos Java se realizar.
Aí veio o Spring chutar-lhes o saco com sua forma sensacional de gerenciar as instâncias e a Microsoft com seu bafo quente de .NET no pescoço deles. Aí Oracle, IBM, Red Hat e outros pararam de frescura e especificaram o fabuloso JEE6, o me perdoe eu melhorei edition, :-).
Transformar uma classe qualquer em uma classe de EJB não poderia ser mais fácil. Segue como fica o MuralResource após modificarmos a mesma para ser um EJB com injeção automática de recursos provenientes do servidor:
package sample.mural; import javax.annotation.Resource; import javax.ejb.Stateless; import javax.sql.DataSource; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; // Esta anotação faz este JAX-RS Resource ser um EJB @Stateless // Esta é do JAX-RS @Path("/mural") public class MuralResource { // Esta anotação só funciona porque isto é um EJB @Resource(name = "jdbc/mural") // Jamais fazemos lookup nisto, ele é "Injetado" pelo servidor private DataSource ds; // este método devolve o total de recados na URI /service/mural/count @GET @Path("/count") public String getMsgCount() throws Exception { return ""+YASQLUtil.getRecadosCount(ds); } // este devolve uma lista de recados. Veremos detalhes mais adiante @GET public RecadoContainer getRecados() throws Exception { return YASQLUtil.getRecados(ds, 1); } // este devolve um offset específico de recados // a URI toma, por exemplo o seguinte formato: /service/mural/3 // em que "3" é o número da página mapeado para uma variável do método // e validado por uma expressão regular no Path @GET @Path("/{id: [1-9]\\d*}") public RecadoContainer getRecadosByPagina(@PathParam("pagina") int pagina) throws Exception { return YASQLUtil.getRecados(ds, pagina); } // cadastro de um novo recado. O objeto é recebido no corpo do POST // e montado automaticamente pelo servidor. @POST public String newRecado(Recado rec) throws Exception { return ""+YASQLUtil.newRecado(ds, rec); } }
Parece bem mais funcional, não? Notem a sutileza da anotação @Stateless: ela informa ao container que ele deve gerenciar as instâncias dessa classe, bem como os acessos à mesma. Não obstante, ele ganha direito a ter um contexto próprio, também gerenciado pelo servidor, de modo que podemos Injetar dentro dele objetos que o servidor de aplicação disponibiliza para os aplicativos nele publicados. O DataSource é um desses objetos. Se você viu o tutorial com Tomcat, a diferença é evidente, até gritante. Vários outros objetos podem ser injetados, outros EJB's, Mail Sessions, Filas JMS e por aí vai.
Abaixo as outras classes utilitárias criadas para auxiliar nosso EJB.
YASQLUtil:
package sample.mural; import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ResourceBundle; import javax.sql.DataSource; /** * YASQLUtil - Yet Another SQL Util. Você com certeza um dia vai ter um. Talvez * tenha. Existem chances. Pode ser que sim, pode ser que não. Eu nego piamente * que você precise de algo assim algum dia! * * @author sombriks * */ public class YASQLUtil { private static final ResourceBundle b = ResourceBundle// .getBundle(YASQLUtil.class.getName()); public static int getRecadosCount(DataSource ds) throws Exception { Connection cn = null; PreparedStatement ps = null; ResultSet rs = null; try { cn = ds.getConnection(); ps = cn.prepareStatement(b.getString("count")); rs = ps.executeQuery(); if (rs.next()) return rs.getInt(1); } finally { close(cn, ps, rs); } return 0; } public static int newRecado(DataSource ds, Recado rec) throws Exception { Connection cn = null; PreparedStatement ps = null; ResultSet rs = null; try { cn = ds.getConnection(); ps = cn.prepareStatement(b.getString("insert"),// PreparedStatement.RETURN_GENERATED_KEYS); ps.setString(1, rec.getMensagem()); ps.setDate(2, new Date(System.currentTimeMillis())); ps.executeUpdate(); rs = ps.getGeneratedKeys(); if (rs.next()) return rs.getInt(1); } finally { close(cn, ps, rs); } return 0; } public static RecadoContainer getRecados(DataSource ds, int pagina) throws Exception { RecadoContainer ret = new RecadoContainer(); Connection cn = null; PreparedStatement ps = null; ResultSet rs = null; try { cn = ds.getConnection(); ps = cn.prepareStatement(b.getString("select")); ps.setInt(1, (pagina - 1) * 10); rs = ps.executeQuery(); while (rs.next()) { Recado r = new Recado(); r.setId(rs.getInt(1)); r.setMensagem(rs.getString(2)); r.setData(rs.getDate(3)); ret.getRetorno().add(r); } } finally { close(cn, ps, rs); } return ret; } private static void close(Connection cn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); if (ps != null) ps.close(); if (cn != null) cn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
Arquivo de propriedades (YASQLUtil.properties):
count = select count(id) from mural insert = insert into mural (mensagem,data) values (?, ?) select = select id,mensagem,data from mural order by id desc limit 10 offset ?
Recado:
package sample.mural; import java.util.Date; import javax.xml.bind.annotation.XmlType; /** * simples pojo de transporte * * @author sombriks * */ @XmlType public class Recado { private int id; private String mensagem; private Date data; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getMensagem() { return mensagem; } public void setMensagem(String mensagem) { this.mensagem = mensagem; } public Date getData() { return data; } public void setData(Date data) { this.data = data; } }
RecadoContainer:
package sample.mural; import java.util.ArrayList; import java.util.List; import javax.xml.bind.annotation.XmlRootElement; /** * classe usada para ser o RootElement * * @author sombriks * */ @XmlRootElement public class RecadoContainer { private Listretorno = new ArrayList (); public List getRetorno() { return retorno; } public void setRetorno(List retorno) { this.retorno = retorno; } }
Exceto pelo YASQLUtil, apinhado de código específico de JDBC, o resto não representa grandes desafios. A classe de Container tem um único propósito: servir de elemento Raíz, pois não podemos usar JAXB para devolver listas diretamente; ele exige um elemento raíz para melhor refletir a estrutura de documento, seja ele XML ou JSON.
Estamos quase prontos para executar o teste deste aplicativo. Devemos agora configurar o DataSource do lado do servidor.
De modo a fazermos nossa aplicação utilizar aquela base de dados criada no mysql lá no início de nossas atividades, precisaremos fazer o download do conector jdbc do mysql; o Slackware não empacota esse driver, logo vamos baixa-lo aqui:
Descompacte o zip, como de costume em um lugar conhecido e fácil de achar. Dentro dele existe um .jar chamado mysql-connector-java-5.1.18-bin.jar, este é o driver do mysql.
Copie este driver para a pasta lib do glassfish. Especificamente a pasta lib que fica dentro do diretório glassfish dentro do diretório do glassfish... falamos disso lá em cima. Se ficar confuso, jogue o .jar do driver dentro da pasta lib dentro da pasta domain1 (exemplo: /home/sombriks/Downloads/glassfish3/glassfish/domains/domain1/lib). Você consegue, eu acredito em você!
Finalmente, você pode rodar o glassfish, pois vamos precisar dele no ar para criar o DataSource. Botão direito sobre o servidor do glassfish na aba servers, opção Start; aguarde o servidor subir totalmente (leva em torno de 15 segundos, até menos) e novamente com botão direito vá em GlassFish>View Admin Console. Se por algum motivo não funcionar, abra o endereço http://localhost:4848/ Diretamente no firefox:
Após a tela de carregamento desaparecer, uma barra lateral cheia de opções aparecerá. Selecione Resources>JDBC>JDBC Connection Pools. Uma tabela surgirá.
O Connection Pool é a parte que "conhece" o banco de dados. Nele é que vão os dados sensíveis do seu ambiente. Selecione o botão "New..." e comecemos a preencher os dados no formulário que surgirá:
- Pool Name: muralPool
- Resource Type: javax.sql.DataSource
- Database Driver Vendor: MySql (e não encoste na caixa de texto vazia abaixo!)
Aperte no botão next, localizado no canto superior direto (WHY???), na tela que surgirá desca até a tabela "Additional Properties (203)" e dentre as centenas de atributos, encontre e informe estes:
- User: mural
- ServerName: localhost
- DatabaseName: mural
- Password: mural
- Url: jdbc:mysql://localhost:3306/mural
- URL: jdbc:mysql://localhost:3306/mural (tem duas, ajuste ambas só por via das dúvidas)
Pressione finish quando acabar. O passo seguinte é Resources>JDBC>JDBC Resources, bem mais simples. Pressione New... e forneça os seguintes dados no formulário:
- JNDI Name: jdbc/mural (o mesmo nome usado na anotação @Resoruce, lá no ejb dentro da aplicação)
- Pool Name: muralPool
Após pressionar OK, o servidor terá um DataSource publicado pronto para ser consumido. Tudo isso para que o desenvolvedor da aplicação não tenha a necessidade de saber muitos detalhes sobre o banco de dados. Sim... de alguma forma, isso é uma coisa boa, mas deixemos este debate filosófico para outro momento.
Já viemos até aqui... Podemos iniciar a aplicação, só pra ver qualé. Reinicie o glassfish, quando ele voltar, aperte o botão direito sobre o projeto do eclipse, Run As>Run on Server. Na tela que surge, selecione o glassfish e pressione Finish. No console a saída deve ser mais ou menos assim:
INFO: Portable JNDI names for EJB MuralResource : [java:global/mural/MuralResource!sample.mural.MuralResource, java:global/mural/MuralResource] INFO: Registering the Jersey servlet application, named sample.mural.App, at the servlet mapping, /service/*, with the Application class of the same name INFO: WEB0671: Loading application [mural] at [/mural] INFO: mural was successfully deployed in 6,745 milliseconds.
Visite http://localhost:8080/mural/service/mural/count e veja a contagem de zero mensagens, ;-)
O passo seguinte é o lado android da solução. Comecemos com o download do Android SDK:
Como já fizemos anteriormente, descompacte em lugar conhecido. Vamos instalar o plugin de eclipse para desenvolver para android, o ADT Plugin for Eclipse:
Basta seguir os passos recomendados na página. De forma resumida, copie a url da caixa em destaque, vá para o eclipse, menu Help>Install New Software>Add... e cole a url no campo Name e no campo Location. pressione Ok e aguarde, o eclipse começará a recuperar as informações do plugin. Quando aparecer a opção Developer Tools, marque o checkbox da mesma e aperte o botão Next e vá apertando Next e aceitando termos de licença e essas coisas menores até o plugin finalmente começar a ser instalado. Quando o eclipse pedir para reiniciar, reinicie o eclipse; por ser "primeiro acesso" ele voltará perguntando onde foi que você descompactou o SDK. Indique o caminho completo selecionando a opção "Use existing SDKs". Após indicar a pasta ele apresentará um alerta, mas você pode ignorar e ir adiante.
E finalmente podemos desenvolver android, certo? errado!
O sistema é 64 bits, e o google só disponibiliza a sdk em 32 bits! Erros estranhos e sem sentido como o descrito abaixo vão aparecer no console do eclipse:
[2011-12-13 18:35:16 - DDMS] DDMS files not found: /home/sombriks/Downloads/eclipse/platform-tools/adb /home/sombriks/Downloads/eclipse/tools/hprof-conv /home/sombriks/Downloads/eclipse/tools/traceview [2011-12-13 18:38:54 - DDMS] DDMS files not found: /home/sombriks/Downloads/android-sdk-linux/platform-tools/adb
Embora erro primário, no slackware você corrige facilmente: baixe o pacote projetado pelo alienBOB e disponibilizado pela comunidade italiana do slacwkare: http://repository.slacky.eu/slackware64-13.37/libraries/compat32-libraries/
Neste link tem uma pasta que dentro dela tem o pacote .txz das bibliotecas de compatibilidade. Faça o download e após isso instale o pacote com o seguinte comando (supondo terminal no mesmo diretório do pacote):
installpkg compat32-libraries-*.txz
Terminada a instalação, feche o eclipse e abra-o novamente. Agora ele virá sem erros... Errado! ainda há um alerta surgindo na tela:
Siga as intruções do alerta, abra o SDK Manager. Caso esteja sem saber o que vem a ser o SDK Manager, você chega nele através daquele botão na toolbar que tem uma caixa com uma seta para baixo, e um robôzinho verde dentro, ;-). Você verá esse ícone ampliando o screenshot anterior. Essa é a tela do SDK Manager:
Marque o checkbox das Tools, e opcionalmente baixe outras versões da SDK do android além da que já se encontra marcada. No meu caso, Baixarei a SDK, Samples e Google API's da versão 2.2:
Pressione o botão Install, que se você observar está contanto os pacotes que você marcou... como se fosse da conta dele!
Depois de um longo tempo fazendo downloads, quando ele acabar feche o SDK Manager, reinicie o eclipse e finalmente criaremos o projeto android:
Batize o projeto como muraldroid, selecione a versão da SDK que desejar (irei deixar a 4.0 marcada pois pode ser que você só tenha essa) e no Package Name coloque sample.android.mural e pressione Finish.
Seu workspace deve estar assim, com os dois projetos nele:
Vamos abrir a pasta res/layout e editar o main.xml, que é o content view da Activity que foi criada automaticamente. Ele deve estar mais ou menos assim:
Troque por este conteúdo aqui:
Crie ainda num novo android xml chamado simpletext.xml, pois iremos precisar dele para renderizar o label da lista de recados:
O android deverá automaticamente renderizar a classe R na pasta gen contento este novo id: simpletext.
Agora vamos para a classe da nossa Activity, a MuraldroidActivity. Vamos adicionar o código que recupera a ListView que adicionamos no leiaute principal e iremos usar um Adapter estático, apenas para vermos a lista e o scroll em ação:
package sample.android.mural; import android.app.Activity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; public class MuraldroidActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ListView listView = (ListView) findViewById(R.id.listView1); ArrayAdapteradapter = // new ArrayAdapter (this, R.layout.simpletext); int i = 30; while (i-- > 0) adapter.add("item " + i); listView.setAdapter(adapter); adapter.notifyDataSetChanged(); } }
Agora com o botão direito sobre o projeto selecione Run As>Android Application; o eclipse vai lhe perguntar se você deseja criar um novo dispositivo; responda Yes e caso você não tenha um aparelho no modo debug por perto, crie um virtual.
Após criar o dispositivo virtual, ele irá carregar... não feche mais o emulador, o segundo "deploy" será mais rápido com o emulador já rodando.
Supondo que você tenha obtido sucesso rodando pelo emulador, algo do tipo deve aparecer:
Legal, né? Vamos agora configurar o AndroidManifest.xml para liberarmos acesso à internet para nossa aplicação. Deixe-o assim:
Crie uma pequena classe auxiliar, vamos chamá-la de ClienteServico. Ela irá fazer uma requisição http para o serviço que temos rodando no glassfish e dessa forma conectar os dois lados do projeto. Ela será um singleton, você pode faze-la da seguinte maneira:
package sample.android.mural; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; public enum ClienteServico { INSTANCE; // NOTA: troque 192.168.0.193 pelo IP da rede interna do seu computador // pois o emulador não está no localhost! private String list = "http://192.168.0.193:8080/mural/service/p%d"; private String save = "http://192.168.0.193:8080/mural/service"; // Recado e RecadoContainer foram copiados do projeto do serviço. // Apague a anotação do @XmlRootElement da classe RecadoContainer // Mude o tipo do campo data de Date para String na classe Recado public RecadoContainer getRecadosByPagina(int page) throws Exception { DefaultHttpClient client = new DefaultHttpClient(); HttpUriRequest req = new HttpGet(String.format(list, page)); req.addHeader("Accept", "application/json");// PROBLEM // TODO terminar return null; } public String newRecado(Recado rec) throws Exception { DefaultHttpClient client = new DefaultHttpClient(); HttpUriRequest req = new HttpPost(save); req.addHeader("Accept", "plain/text"); // TODO terminar return null; } }
Este é o ponto em que eu finalmente queria chegar. Aqui, o caminho a seguir seria baixar o Gson e colocar como dependência do projeto android. Entretanto, alguns problemas de ordem prática vão ocorrer que nos levarão a não usar essa biblioteca. Considere o json válido abaixo:
{"retorno":[ {"data":"2011-12-10T00:00:00-03:00","id":"1","mensagem":"olá pessoas!"}, {"data":"2011-12-10T00:00:00-03:00","id":"2","mensagem":"como estão vocês?"}, {"data":"2011-12-10T00:00:00-03:00","id":"3","mensagem":"recado rápido!"} ]}
Isto é o retorno gerado pelo serviço REST do glassfish quando pedimos a ele a lista de recados. Não precisa acreditar em mim, você pode dar carga manualmente no banco e requisitar ao serviço este JSON:
Carga no banco:
insert into mural (mensagem,data) values ('olá pessoas!','2011-12-10'); insert into mural (mensagem,data) values ('como estão vocês?','2011-12-10'); insert into mural (mensagem,data) values ('recado rápido!','2011-12-10');
Comando rápido para requisitar JSON no terminal do slackware:
curl -H "Accept:application/json" -X GET http://localhost:8080/mural/service/mural
Se quiser que seja entregue XML:
curl -H "Accept:text/xml" -X GET http://localhost:8080/mural/service/mural
Veja como é a saída xml:
2011-12-10T00:00:00-03:00 1 ola pessoas! 2011-12-10T00:00:00-03:00 2 como estao voces? 2011-12-10T00:00:00-03:00 3 recado rápido!
Notaram a diferença? O elemento que contém a lista, na versão XML está implícito. Já na versão JSON, ele se chama retorno e tem uma lista válida, tudo feliz e normal.
Agora delete dois dos três registros do banco:
delete from mural where id in (1,2);
Feito isso, use novamente o curl para recuperar o XML... e recupere em seguida o JSON. As saídas serão as seguintes:
2011-12-10T00:00:00-03:00 3 recado rápido!
{"retorno": {"data":"2011-12-10T00:00:00-03:00","id":"3","mensagem":"recado rápido!"} }
A diferença é sutil, porém decisiva. A versão JSON perdeu a característica da lista; sim, poderíamos considerar que a lista tornou-se tão implícita quanto a versão XML, mas é fácil provar que esta atual versão JSON não tem condições de diferenciar entre um elemento único e uma lista de elementos, como outrora era claro. Curto e grosso: a versão JSON perde informação nestes casos específicos.
Mas este não é o problema maior.
Sua aplicação android É escrita em java, e em java a definição dos atributos é estática. Poderíamos utilizar saídas do próprio JAXB, mas não existe JAXB para android; se quiser adiciona-lo manualmente, serão pouco mais de nove mega de download e aproximadamente 5 mega de dependências... talvez menos, mas certamente será maior que toda a sua aplicação. Diversão mesmo é tentar usar o JAXB no android, tem um easter egg esperando os mais ousados, mas não é agradável.
Por isso, embora tentador, não use json caso você não tenha tempo de escrever um parser customizado e deseja mesmo converter a stream de forma transparente de e para o serviço. A biblioteca simplexml e o header HTTP ajustado para receber XML farão o serviço funcionar muito mais rapidamente.
Modifique o ClienteServico para ficar assim:
package sample.android.mural; import java.io.ByteArrayOutputStream; import java.io.InputStream; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.simpleframework.xml.Serializer; import org.simpleframework.xml.core.Persister; public enum ClienteServico { INSTANCE; // NOTA: troque 192.168.0.193 pelo IP da rede interna do seu computador // pois o emulador não está no localhost! private String list = "http://192.168.0.193:8080/mural/service/p%d"; private String save = "http://192.168.0.193:8080/mural/service"; // Recado e RecadoContainer foram copiados do projeto do serviço. // Apague a anotação do @XmlRootElement da classe RecadoContainer public RecadoContainer getRecadosByPagina(int page) throws Exception { DefaultHttpClient client = new DefaultHttpClient(); HttpUriRequest req = new HttpGet(String.format(list, page)); req.addHeader("Accept", "text/xml"); HttpResponse res = client.execute(req); InputStream in = res.getEntity().getContent(); Serializer ser = new Persister(); RecadoContainer c = ser.read(RecadoContainer.class, in); in.close(); return c; } public String newRecado(Recado rec) throws Exception { Serializer ser = new Persister(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ser.write(rec, baos); DefaultHttpClient client = new DefaultHttpClient(); HttpPost req = new HttpPost(save); req.setEntity(new StringEntity(new String(baos.toByteArray(), "UTF-8"))); req.addHeader("Content-type", "text/xml"); req.addHeader("Accept", "plain/text"); HttpResponse res = client.execute(req); InputStream in = res.getEntity().getContent(); int i = -1; String ret = ""; byte[] buf = new byte[1024]; while ((i = in.read(buf)) > -1) ret += new String(buf, 0, i); return ret; } }
E a nossa activity deve fazer uso destas chamadas de serviço. Modifique-a para ficar assim:
package sample.android.mural; import java.util.List; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.Toast; public class MuraldroidActivity extends Activity { private ListView mensagensView; private EditText txtMensagem; private Button buttonPostar; private RecadoListAdapter listAdapter; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // referências mensagensView = (ListView) findViewById(R.id.listView1); txtMensagem = (EditText) findViewById(R.id.editText1); buttonPostar = (Button) findViewById(R.id.button1); // eventos e modelos buttonPostar.setOnClickListener(new OnClickListener() { public void onClick(View v) { salvarRecado(); listAdapter.clear(); updateMural(); } }); listAdapter = new RecadoListAdapter(this); mensagensView.setAdapter(listAdapter); updateMural(); } private void updateMural() { AsyncTask> at = // new AsyncTask >() { @Override protected List doInBackground(Void... params) { try { return ClienteServico.INSTANCE// .getRecadosByPagina(1).getRetorno(); } catch (Exception e) { e.printStackTrace(); } return null; } }.execute(); try { listAdapter.addRecados(at.get()); } catch (Exception e) { e.printStackTrace(); } } /* * utilitário para salvar os recados */ private void salvarRecado() { AsyncTask at = // new AsyncTask () { @Override protected String doInBackground(Void... params) { Recado r = new Recado(); r.setMensagem(txtMensagem.getText().toString()); String s = null; try { s = ClienteServico.INSTANCE.newRecado(r); } catch (Exception e) { e.printStackTrace(); } return s; } }.execute(); try { Toast.makeText(MuraldroidActivity.this, "Mensagem " + at.get() + " cadastrada", // Toast.LENGTH_SHORT).show(); } catch (Exception e1) { e1.printStackTrace(); } } }
Observem o uso do AsyncTask para fazer as requisições ao serviço: a partir da versão 3 do android, você não pode mais fazer operações de IO blocante na thread principal e nem pode modificar a interface gráfica a partir de threads que não sejam a principal. Daí o uso de AsyncTask.
E abaixo os beans de domínio com os ajustes para escapar dos problemas com conversão de data e com as anotações do simplexml.
Classe Recado:
package sample.android.mural; import org.simpleframework.xml.Default; /** * simples pojo de transporte * * @author sombriks * */ @Default(required=false) public class Recado { private int id; private String mensagem; private String data; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getMensagem() { return mensagem; } public void setMensagem(String mensagem) { this.mensagem = mensagem; } public String getData() { return data; } public void setData(String data) { this.data = data; } }
Classe RecadoContainer:
package sample.android.mural; import java.util.ArrayList; import java.util.List; import org.simpleframework.xml.Default; import org.simpleframework.xml.ElementList; /** * * @author sombriks * */ @Default(required=false) public class RecadoContainer { @ElementList(inline = true, entry = "retorno", required = false) private Listretorno = new ArrayList (); public List getRetorno() { return retorno; } public void setRetorno(List retorno) { this.retorno = retorno; } }
Sem esquecer do ListAdapter que implementamos para a ocasião:
package sample.android.mural; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; public class RecadoListAdapter extends BaseAdapter { private Listrecados = new ArrayList (); private LayoutInflater inflater; public RecadoListAdapter(Activity ctx) { inflater = (LayoutInflater) ctx .getSystemService(Context.LAYOUT_INFLATER_SERVICE); } public void clear() { recados.removeAll(recados); notifyDataSetChanged(); } public void addRecados(List recados) { this.recados.addAll(recados); notifyDataSetChanged(); } @Override public int getCount() { return recados.size(); } @Override public Object getItem(int position) { return recados.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { Recado r = recados.get(position); System.out.println(r.getData()); if (convertView == null) convertView = inflater.inflate(R.layout.simpletext, null); TextView t = (TextView) convertView; System.out.println(t); t.setText(r.getMensagem()); return convertView; } }
Sem mais, esta é uma boa abordagem para o consumo de serviços REST, sinta-se à vontade para testar e variar detalhes disso.
E boa sorte!
Nenhum comentário :
Postar um comentário