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 List retorno = 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);
ArrayAdapter adapter = //
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 List retorno = 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 List recados = 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