segunda-feira, 16 de abril de 2012

Animações no Android 3+

Olá povo,

Essa semana, meu colega Felipe Vasconcelos me lembrou de um problema existente na API de animações do Android 2 (que falei nesse post).
Digamos que você tenha um botão em uma posição x/y, e que esse botão seja posicionado em outro lugar através de uma animação (uma TranslateAnimation pra ser mais exato). Se você clicar nesse botão após a animação, você não conseguirá. Isso se deve a uma característica (ou bug?) da API de animação do Android até o 2.x, que só altera o aspecto da View, mas não seu conteúdo interno. Isso quer dizer que, apesar do botão mudar a posição visualmente, ele continua na posição antiga. Ou seja, se você clicar no botão na posição anterior à animação, o evento de clique ocorrerá.

Para corrigir esse problema, o Google introduziu uma nova API de animação no Android 3.0, que traz ainda o benefício de acompanhar cada passo da animação, coisa que não acontecia na antiga API.
Para entender melhor o problema e a nova API, vamos criar um exemplo bem simples. Abaixo temos o arquivo XML que define a tela da nossa aplicação:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="horizontal">

  <Button
    android:id="@+id/btnAnimacaoAntiga"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onClickAnimarAntigo"
    android:text="Animar 2.x" />

  <Button
    android:id="@+id/btnAnimarNova"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onClickAnimarNovo"
    android:text="Animar Honeycomb" />

</LinearLayout>
A tela é bem simples, apenas dois botões que serão animados até a posição 200 na coordenada Y. O primeiro animará utilizando a API antiga, e o segundo a nova. Abaixo, temos a Activity da aplicação:
public class AnimationHoneycombActivity 
  extends Activity {
 
  private boolean b1Desceu;
  private boolean b2Desceu;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }
    
  public void onClickAnimarAntigo(View v){
    int yInicial, yFinal;
    if (b1Desceu){
      yInicial = 200;
      yFinal = 0;
    } else {
      yInicial = 0;
      yFinal = 200;
    }
     
    TranslateAnimation animacao = 
      new TranslateAnimation(0, 0, yInicial, yFinal); 
    animacao.setDuration(2000);
    animacao.setFillAfter(true);
    v.startAnimation(animacao);
    b1Desceu = !b1Desceu;
  }
    
  public void onClickAnimarNovo(final View v){
      
    ValueAnimator animacao = 
      ValueAnimator.ofFloat(0, 200);

    animacao.setTarget(v);
    animacao.setDuration(2000);
    animacao.addUpdateListener(
      new ValueAnimator.AnimatorUpdateListener() {
   
        @Override
        public void onAnimationUpdate(
          ValueAnimator animation) {

          Float valor = (Float)
            animation.getAnimatedValue();
          v.setY(valor);
       }
     });

    if (b2Desceu){
      animacao.reverse();
    } else {
      animacao.start();
    }
    b2Desceu = !b2Desceu;
  }
}
No primeiro método, utilizamos a API antiga de animações. Utilizei uma flag para determinar se o botão desceu, para determinar a posição inicial e final na coordenada Y. Em seguida, criei uma TranslateAnimation, defini a duração, e em seguida, com o método setFillAfter determinei que ao terminar a animação, o botão deve permanecer onde a animação terminou. Por fim, mandei animar o botão.
Já no segundo método, estou usando a nova API de animações. A classe ValueAnimator é a chave dessa API, e ela tem basicamente: um valor inicial e final; um target, que é um objeto View; a duração em milissegundos; e um AnimatorUpdateListener. Esse último objeto é chamado N vezes para realizar a animação, onde N depende do valor final e sua respectiva duração. É nesse objeto que devemos alterar a propriedade da View que queremos animar, que no nosso caso, é a propriedade Y do botão.
No final do método vemos se o botão desceu, em caso positivo, solicitamos que a animação seja feita ao contrário com o método reverse; caso contrário, mandamos animar para a posição desejada.
Para observar o "bug" da antiga API de animação, clique no primeiro botão e ele descerá. Em seguida, clique no botão e você notará que nada acontecerá. Agora tente clicar onde o botão estava... Magicamente o botão voltará para a posição original.

Em resumo, se você precisar interagir com uma View que sofreu uma animação, ou ainda precisar obter informações internas sobre elas, você terá que contornar essa limitação da API antiga de animações. Mas se você estiver pensando em desenvolver para tablets Android ou ainda para os novos aparelhos com Android 4 ICS, utilize a nova API de animações.

4br4ç05,
nglauber

sábado, 14 de abril de 2012

iOS: Lendo XML

Olá povo,

Neste post vou mostrar como ler arquivos XML em aplicações iOS. O XML que estou lendo, eu baixei no site do livro Google Android do Ricardo Lecheta (eu tento, mas não deixo o Android :)
O modelo do estrutura do XML que iremos ler é apresentado abaixo:
<?xml version="1.0" encoding="utf-8"?>
<carros>
  <carro>
    <nome>Modelo do Carro</nome>
    <desc>Descrição do Carro</desc>
    <url_info>http://www.ferrari.com/</url_info>
    <url_foto>http://www.livroandroid.com.br/</url_foto>
  </carro>
  ...
</carros>
Crie um novo projeto no Xcode utilizando o template "Single View Application". Agora acesse o site do livro e baixe o XML, em seguida adicione-o ao seu projeto no Xcode na pasta "Supporting Files". A estrutura do projeto (no final) deve ficar conforme abaixo:

Como você deve ter observado, o carro descrito no XML tem os atributos nome, desc, url_info e url_foto. Vamos criar uma classe que representará esse objeto em código.
// Carro.h ---------------------------------------
#import <Foundation/Foundation.h>

@interface Carro : NSObject

@property (strong, nonatomic) UIImage *image;
@property (strong, nonatomic) NSString *nome;
@property (strong, nonatomic) NSString *descricao;
@property (strong, nonatomic) NSString *urlInfo;
@property (strong, nonatomic) NSString *urlFoto;

@end
// Carro.m ---------------------------------------
#import "Carro.h"

@implementation Carro

@synthesize nome, descricao, urlFoto, urlInfo, image;

@end
Vou alterar o AppDelegate da aplicação para ficar desta forma:
// NGAppDelegate.h -------------------------------
#import <UIKit/UIKit.h>

@class NGViewController;

@interface NGAppDelegate : 
  UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) 
  UIWindow *window;
@property (strong, nonatomic) 
  UINavigationController *navigationController;

@end
// NGAppDelegate.m -------------------------------
#import "NGAppDelegate.h"
#import "NGViewController.h"

@implementation NGAppDelegate

@synthesize window = _window;
@synthesize navigationController;

- (BOOL)application:
   (UIApplication *)application
     didFinishLaunchingWithOptions:
   (NSDictionary *)launchOptions{

  self.window = [[UIWindow alloc]
     initWithFrame:[[UIScreen mainScreen] bounds]];

  NGViewController *viewController =
     [[NGViewController alloc] initWithNibName:
       @"NGViewController" bundle:nil];

  navigationController =
     [[UINavigationController alloc]
       initWithRootViewController:viewController];

  self.window.rootViewController =
     self.navigationController;

  [self.window makeKeyAndVisible];
  return YES;
}
// Outros métodos não alterados
@end
Nesse template, a aplicação é criada com uma UIViewController simples. Vamos utilizar uma UITableViewController que listará os dados lidos através de um objeto da class NSXMLParser. Sua utilização é bem similar ao SAX do Java, e teremos que implementar um protocolo NSXMLParserDelegate, e a medida que as tags são lidas os métodos desse protocolo são chamados.
#import <UIKit/UIKit.h>
#import "Carro.h"

@interface NGViewController : UITableViewController
   <NSXMLParserDelegate>{

  NSMutableArray *carros;
  Carro *carro;
  NSString *currTag;
}

@end
Um NSMutableArray armazenará os objetos Carro criados a partir da leitura do XML. Durante o parser do XML, precisamos saber que tag está sendo lida, para tal setarmos o atributo currTag com tag que corresponde a propriedade da classe Carro. Por exemplo, quando o parser encontrar uma tag "carro", criamos um objeto Carro, já quando encontramos uma tag "nome", devemos definir o a propriedade nome do objeto carro. Por fim, ao encontrar a tag "/carro" devemos adicionar o objeto carro na lista.
Vamos agora a implementação da tela (lembrando que só estamos mostrando os métodos alterados):
#import "NGViewController.h"
#import "Carro.h"

@implementation NGViewController

- (void)viewDidLoad{
  [super viewDidLoad];
  self.title = @"Carros";
      carros = [[NSMutableArray alloc] init];
  // Pega o caminho do arquivo XML
  NSString *xmlPath = [[NSBundle mainBundle]
     pathForResource:@"carros" ofType:@"xml"];
  // Obtém os bytes do arquivo
  NSData *xmlData =
     [NSData dataWithContentsOfFile:xmlPath];
  // Cria o objeto que fará o parser do XML
  NSXMLParser *xmlParser =
     [[NSXMLParser alloc] initWithData:xmlData];
  [xmlParser setDelegate:self];
  [xmlParser parse];
}

// Métodos chamado quando NSXMLParser
// inicia a leitura de uma TAG
-(void)parser:(NSXMLParser *)parser
   didStartElement:(NSString *)elementName
   namespaceURI:(NSString *)namespaceURI
   qualifiedName:(NSString *)qName
   attributes:(NSDictionary *)attributeDict {

  currTag = elementName;      
  // Se achou a TAG carro, crie um novo objeto
  if ([currTag compare:@"carro"] == NSOrderedSame){
    carro = [[Carro alloc] init];  
  }
}

// Método chamado quando vai ler o 
// conteúdo da TAG
-(void)parser:(NSXMLParser *)parser
    foundCharacters:(NSString *)string {

  if ([currTag compare:@"nome"] ==
     NSOrderedSame && carro.nome == nil){
    carro.nome = string;
  } else if ([currTag compare:@"desc"] ==
     NSOrderedSame && carro.descricao == nil){
    carro.descricao = [string
       stringByTrimmingCharactersInSet:
        [NSCharacterSet
           whitespaceAndNewlineCharacterSet]];
  } else if ([currTag compare:@"url_info"] ==
     NSOrderedSame && carro.urlInfo == nil){
    carro.urlInfo = [string
       stringByTrimmingCharactersInSet:
        [NSCharacterSet
            whitespaceAndNewlineCharacterSet]];
  } else if ([currTag compare:@"url_foto"] ==
     NSOrderedSame && carro.urlFoto == nil){
    carro.urlFoto = [string
       stringByTrimmingCharactersInSet:
        [NSCharacterSet
           whitespaceAndNewlineCharacterSet]];
  }
}

// Método chamado quando termina de ler uma tag
-(void)parser:(NSXMLParser *)parser
   didEndElement:(NSString *)elementName
   namespaceURI:(NSString *)namespaceURI
   qualifiedName:(NSString *)qName {
  // Se a tag lida for "carro", adiciona à lista
  if ([elementName compare:@"carro"]==NSOrderedSame){
    [carros addObject:carro];
  }
}

// Método de UITableViewController
-(NSInteger)tableView:(UITableView *)tableView
   numberOfRowsInSection:(NSInteger)section {
  return [carros count];
}

-(NSInteger)numberOfSectionsInTableView:
  (UITableView *)tableView {
  return 1;
}

-(UITableViewCell *)tableView:(UITableView *)
  tableView cellForRowAtIndexPath:
   (NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @"Cell";
  UITableViewCell *cell = [tableView
    dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
    cell = [[UITableViewCell alloc]
       initWithStyle:UITableViewCellStyleSubtitle
       reuseIdentifier:CellIdentifier];
  }
  Carro *_carro = [carros objectAtIndex:indexPath.row];
  cell.textLabel.text = _carro.nome;
  cell.detailTextLabel.text = _carro.urlInfo;
  return cell;
}

-(void)tableView:(UITableView *)tableView
   didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
  // Obtém o carro da linha selecionada
  Carro *_carro = [carros objectAtIndex:indexPath.row];
  // Abre o site do carro selecionado  
  NSURL *url = [NSURL URLWithString:_carro.urlInfo];
  [[UIApplication sharedApplication] openURL:url];
}
@end
O código acima está comentado e parte relacionada com a UITableViwController foi detalhada nesse post aqui.

Feito isso, precisamos fazer alguns ajustes no NIB. Abra o NGViewController.xib e remova a UIView e adicione uma UITableViewController. Ligue esse componente ao Outlet da propriedade view, e o dataSource e delegate.
Além disso, alterei também a propriedade "Top Bar" para "Navigation Bar". Feito isso, podemos rodar nossa aplicação. O resultado é apresentado abaixo.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

domingo, 8 de abril de 2012

Artigo "Android 4: Ice Cream Sandwich" (parte 2)

Olá povo,

A revista Java Magazine, traz em sua edição de número 102, a segunda (e última) parte da matéria sobre a nova versão da plataforma Android. O texto foi escrito por mim e pelo meu colega Bruno Vinícius.

Nesta segunda parte do artigo, apresentamos mais recursos da nova versão do Android. Abordamos a integração com serviços de agendamento de tarefas, novos componentes de interface gráfica e a vasta gama de possibilidades multimídia e de comunicação trazidas por essa nova versão da plataforma.

Espero que vocês gostem.

4br4ç05,
nglauber

terça-feira, 3 de abril de 2012

iOS: Internacionalização

Olá povo,

Aqui vai o primeiro vídeo-post aqui do blog: Internacionalização no iOS.


Espero que vocês gostem. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sábado, 31 de março de 2012

iOS: UITableView

Olá povo,

Depois de estudar por algumas vezes a programação para a plataforma iOS, resolvi escrever uma série de posts sobre o assunto. Como eu já fiz um "Hello World" e um exemplo básico de tratamento de evento (aqui e aqui respectivamente) vou começar falando do componente UITableView. Esse componente serve para exibir informações em uma lista.
Vamos começar criando um novo projeto no XCode. Selecione File > New > New Project... Na janela que for exibida, selecione Empty Application. Depois preencha os campos conforme a imagem abaixo.

Clique em Next e depois selecione em que diretório deseja salvar o projeto e clique em Create. Será criado um projeto vazio com a estrutura mostrada abaixo:
Neste momento o projeto não faz nada, então vamos criar a primeira tela que exibirá uma lista de nomes. Para tal, clique com o botão direito sobre o projeto e selecione New File...
Selecione UIViewController subclass e clique em Next. No campo Class, preencha com ListagemViewController e em Subclass of, coloque UITableViewController. Clique em Next e em seguida, Create. Criada a classe que representará a primeira tela da aplicação vamos alterar o NGAppDelegate para instanciar nossa tela. No arquivo .h adicione a propriedade do tipo UINavigationController. Ela servirá para abrirmos uma outra tela e já controlar o fluxo entre elas.
#import <UIKit/UIKit.h>

@interface NGAppDelegate : 
  UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) 
  UIWindow *window;

@property (strong, nonatomic) 
  UINavigationController *navegador;

@end
No arquivo .m já foram criados alguns métodos pelo template do XCode, mas só vamos mexer no application:didFinishLaunchingWithOptions.
#import "NGAppDelegate.h"
#import "ListagemViewController.h"

@implementation NGAppDelegate

@synthesize window = _window;
@synthesize navegador;

- (BOOL)application:(UIApplication *)application 
  didFinishLaunchingWithOptions:(NSDictionary *)opts
{
  self.window = [[UIWindow alloc] initWithFrame:[
    [UIScreen mainScreen] bounds]];

    
  ListagemViewController *lista = 
    [[ListagemViewController alloc] init];

  navegador = [[UINavigationController alloc]
    initWithRootViewController:lista];
    
  self.window.backgroundColor = [UIColor whiteColor];
    
  self.window.rootViewController = navegador;
    
  [self.window makeKeyAndVisible];
  return YES;
}
Nesse método instanciamos a nossa tela, e em seguida instanciamos o UiNavigationController passando a nossa tela como tela "raiz", ou seja, a principal. Depois associamos o UINavigationController ao objeto UIWindow, que representa a tela do aparelho.
Neste ponto, você já pode mandar rodar a aplicação, mas não teremos nada para listar. Então vamos a implementação da nossa listagem. No arquivo ListagemViewController.h, declare um array chamado nomes, conforme abaixo:
#import <UIKit/UIKit.h>

@interface ListagemViewController : 
  UITableViewController {

  NSArray *nomes;
}

@end
Como podemos observar, nossa classe herda de UITableViewController. Essa classe implementa dois protocolos (que em Java são interfaces) UITableViewDelegate e UITableViewDataSource. O primeiro trata de eventos disparados pela lista e o segundo define métodos que irão prover informações para a lista. Ao abrirmos o arquivo, podemos notar que temos vários métodos implementados, por isso só vou colocar no código abaixo os métodos que teremos que modificar.
#import "ListagemViewController.h"

@implementation ListagemViewController

#pragma mark - View lifecycle

- (void)viewDidLoad
{
  [super viewDidLoad];
    
  self.navigationItem.title = @"Listagem";
    
  nomes = [NSArray arrayWithObjects:
    @"Nelson", @"Glauber", 
    @"Vasconcelos", @"Leal", nil];
}

// ... um monte de métodos :)

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:
  (UITableView *)tableView
{
  return 1;
}

- (NSInteger)tableView:(UITableView *)tableView 
  numberOfRowsInSection:(NSInteger)section
{
  return nomes.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
  cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  static NSString *CellIdentifier = @"Cell";
    
  UITableViewCell *cell = [tableView 
    dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
    cell = [[UITableViewCell alloc] 
      initWithStyle:UITableViewCellStyleDefault 
      reuseIdentifier:CellIdentifier];
  }

  cell.textLabel.text = 
    [nomes objectAtIndex:indexPath.row];
    
  return cell;
}

@end
No método viewDidLoad, inicializamos nossa lista de nomes e alteramos o título da tela. Já no método numberOfSectionsInTableView retornamos a quantidade de sessões que a lista terá. As sessões servem para agrupar opções de uma lista, mas no nosso caso, teremos apenas uma. No método tableView:numberOfRowsInSection retornamos quantas linhas têm cada sessão, como só temos uma, retornamos a quantidade de itens do nosso array de pessoas.
O último método que alteramos foi o tableView:cellForRowAtIndexPath, ele cria um objeto UITableViewCell para cada item da lista. Para evitar a criação de muitos objetos ele tenta reaproveitar linhas que não estejam mais visíveis na tela. Isso é feito no método dequeueReusableCellWithIdentifier:CellIdentifier. Caso não haja uma linha pra reciclar, criamos uma UITableViewCell com o estilo padrão (UITableViewCellStyleDefault). Em seguida, alteramos o texto da UITableViewCell (que internamente contém um UILabel) utilizando o nosso array de nomes. Para obter a posição a ser exibida, utilizamos a propriedade row o parâmetro indexPath. Pronto! Basta rodar nossa aplicação, e o resultado deverá ficar como abaixo:
Vamos criar uma tela para exibir o item selecionado. Botão direito no projeto, New File... Selecione UIViewController subclass (como fizemos anteriormente) e clique em Next. O nome da classe será DetalheViewController e será subclasse de UIViewController. Marque a opção With XIB for user interface. Clique em Next e depois em Create.
Abra o DetalheViewController.xib e arraste um Label para a tela e faça os ajustes de posicionamento e tamanho que desejar. Em seguida, vamos criar o IBOutlet para esse label: clique com o botão direito sobre o botão, e em Referencing Outlets clique em New Referencing Outlet e arraste para o arquivo .h.
Será exibido um popup para preencher o nome do nosso Outlet. Preencha com txtDetalhe e clique em Connect. Em seguida, vou adicionar a propriedade "texto" para essa tela que será atribuída pela tela de listagem. Quando clicarmos em um item da lista, criaremos uma instância de DetalheViewController a atribuiremos essa propriedade com o item selecionado da lista. O arquivos .h e .m deverão ficar como abaixo (lembrando só listamos o que foi modificado).
#import <UIKit/UIKit.h>

@interface DetalheViewController : UIViewController

@property (weak, nonatomic) 
  IBOutlet UILabel *txtDetalhe;

@property (strong, nonatomic) NSString *texto;

@end
#import "DetalheViewController.h"

@implementation DetalheViewController
@synthesize txtDetalhe, texto;

- (void)viewDidLoad
{
    [super viewDidLoad];
    txtDetalhe.text = texto;
}
Estamos quase lá. Agora volte ao arquivo ListagemController.m implemente o método tableView:didSelectRowAtIndexPath que é o método chamado quando clicamos em um item da lista.
// Adicionar import no começo do arquivo
#import "DetalheViewController.h"

- (void)tableView:(UITableView *)tableView 
  didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  DetalheViewController *detailViewController = 
    [[DetalheViewController alloc] initWithNibName:
      @"DetalheViewController" bundle:nil];
     
  detailViewController.texto = 
    [nomes objectAtIndex:indexPath.row];

  [self.navigationController pushViewController:
    detailViewController animated:YES]; 
}
O método acima instancia nosso DetalheViewController passando o arquivo *.xib (sem a extensão), em seguida atribui a propriedade texto e utiliza o navigationController (que criamos no AppDelegate) para exibir a tela. O resultado pode ser visto abaixo:
Podemos notar que o navigation controller já coloca um botão para voltarmos para a tela anterior. Em breve devo colocar mais posts sobre iOS. Qualquer dúvida, deixem seus comentários. 4br4ç05, nglauber

sexta-feira, 23 de março de 2012

Java Custom Annotations

Olá povo,

Depois de um bom tempo sem postar, vou falar um pouquinho sobre anotações em Java. Quem já programa em Java, já deve ter visto algo como @Override, @Deprecated, @SupressWarnings, ou algo do tipo. Estas são anotações adicionadas ao código-fonte que podem ter funcionalidade meramente indicativa ou auxiliar o compilador Java. Mas um recurso bacana das anotações é a capacidade de utilizá-las em tempo de execução para ajudar na lógica do sistema, permitindo inclusive criar bibliotecas reutilizáveis.
Um framework muito popular que é construído baseado em anotações é o Hibernate (que implementa a especificação JPA, Java Persistence API). Ele permite a persistência de qualquer tipo de objeto, desde que as classes desses objetos estejam com as devidas anotações.

Para ilustrar o uso das anotações, vou criar um mini-mini-mini Hibertnate :) Nele definiremos duas anotações: uma para determinar qual tabela a classe vai persistir seus objetos; e a segunda mapeará um atributo da classe a um campo da tabela.

Vamos começar pelas anotações:
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DBTable {
  public String table();
}

A diferença básica de uma interface comum e uma annotation é o símbolo de @ (arroba) antes da palavra interface. Os "atributos" da anotação são métodos, que no exemplo acima é o nome da tabela no banco. As regras para os atributos/métodos é que eles só podem retornar: primitivos, Strings, enum, Class ou um array dos anteriores.

A anotação @Target indica para qual tipo de elemento java a anotação que estamos definindo irá tratar. Ele pode ser:
TYPE: para classes, interfaces, ou enums;
FIELD: para atributos;
METHOD: para métodos;
PARAMETER: para parâmetros de métodos;
CONSTRUCTOR: em construtores;
LOCAL_VARIABLE: em variáveis locais;
ANNOTATION_TYPE: em uma outra anotação;
PACKAGE: nos pacotes java.

Já a anotação @Retention indica por quanto tempo a anotação será retida. Pode assumir os valores:
RUNTIME: as anotações são armazenadas na classe e estão disponíveis em tempo de execução;
SOURCE: as anotações são armazenadas na classe, mas NÃO estão disponíveis em tempo de execução;
CLASS: as anotações NÃO são armazenadas na classe (default).

Por último, a anotação @Inherited indica que as subclasses herdarão essa anotação.

A anotação acima servirá para mapear uma classe em uma tabela do banco de dados.
Ela será utilizada apenas em classes, ficará disponível em tempo de execução e poderá ser herdada por subclasses que a usarem.

Vamos agora para a anotação que mapeará atributos em campos da tabela:
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBField {
  String colummn() default "";
}

A anotação acima, é apenas para atributos, terá sua informação disponível em tempo de execução e terá o atributo column. Definimos um valor default para a coluna, para que o nome do atributo seja igual ao nome do campo da tabela, não seja preciso preencher.
Vamos agora usar essas anotações em duas classes: Pessoa e Funcionário. Essa segunda herdando da primeira:
@DBTable(table="TBPessoa")
public class Pessoa {

  @DBField(colummn="str_nome")
  private String nome;

  @DBField(colummn="str_end")
  private String endereco;

  @DBField
  private int idade;

  public Pessoa(String nome,
    String endereco, int idade) {

    this.nome = nome;
    this.endereco = endereco;
    this.idade = idade;
  }

  public String getNome() {
    return nome;
  }
  public String getEndereco() {
    return endereco;
  }
  public int getIdade() {
    return idade;
  }
}

O mais importante dessa classe é o mapeamento que fizemos da classe com o nome da tabela e dos atributos com os campos. Tudo isso só usando nossas próprias anotações.
Agora a classe Funcionario:
@DBTable(table="TBFunc")
public class Funcionario extends Pessoa {

  @DBField(colummn="num_ctps")
  String ctps;

  public Funcionario(String nome, String endereco,
    int idade, String ctps) {

    super(nome, endereco, idade);
    this.ctps = ctps;
  }
  public String getCtps() {
    return ctps;
  }
}

Agora vamos criar a classe que lerá objetos (de qualquer classe) e procurará nossas anotações para fazer uma inclusão (FAKE claro :) no banco de dados.
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Set;

public class Repositorio {

  public void insert(Object obj) throws Throwable {
    // Tenta obter nossa anotação na classe
    DBTable persistable =
      obj.getClass().getAnnotation(DBTable.class);

    // Se tiver a anotação...
    if (persistable != null){
      String tabela = persistable.table();
      // Map para montar o SQL campo/valor :)
      HashMap<String, String> keyAndValues =
        new HashMap<String, String>();

      // Obtém os atributos da classe via reflection  
      Field[] fields = getFields(obj.getClass());
      for (Field field : fields) {
        // como os atributos são private,
        // setamos ele como visible
        field.setAccessible(true);
        DBField coluna = 
          field.getAnnotation(DBField.class);

        // Se o atributo tem a anotação
        if (coluna != null){
          // Verifica se está vazio pra usar o
          // nome do próprio atributo
          String columnName = 
            coluna.colummn().equals("") ?
            field.getName() : coluna.colummn();
       
          // Adiciona campo/valor no map
          keyAndValues.put(
            columnName, field.get(obj).toString());
        }
      }
 
      // Varre o map para montar o SQL
      String values = "";
      Set<String> keys = keyAndValues.keySet();
      for (String campo: keys) {
        if (!values.equals("")) values += ",";
        String valor = keyAndValues.get(campo);
        values += campo +"='"+ valor +"'";
      }

      String sql = "INSERT INTO "+ tabela +
        " values("+ values +")";
      System.out.println("SQL---->"+ sql);
    }
  }

  // Método recursivo para obter os atributos da 
  // class e da superclasse
  public Field[] getFields(Class c){
    // Se tem superclasse
    if (c.getSuperclass() != null){
      // Chama o próprio método para pegar os 
      // atributos da superclasse
      Field[] superClassFields = 
        getFields(c.getSuperclass());
      // Pega os atributos da própria classe
      Field[] thisFields = c.getDeclaredFields();
 
      // array com todos os atributos
      Field[] allFields = new Field[
        superClassFields.length +
        thisFields.length];

      // Copia os atributos da superclasse 
      System.arraycopy(superClassFields, 0,
        allFields, 0, superClassFields.length);
      // Copia os atributos da classe atual
      System.arraycopy(thisFields, 0, allFields,
        superClassFields.length, thisFields.length);
   
      return allFields;

    // Se não tem superclasse, retorna os 
    // próprios atributos
    } else {
      return c.getDeclaredFields();
    }
  }
}

O código acima está todo comentado. Qualquer dúvida, deixem seus comentários... Ah mas ainda tem a classe que usa isso tudo :)
public class Main {

  public static void main(String[] args) {
    Repositorio repo = new Repositorio();

    Pessoa p = new Pessoa("Nelson", "Rua tal", 28);
    Funcionario f = new Funcionario(
      "Glauber", "Rua x", 18, "1234");

    try {
      repo.insert(p);
      repo.insert(f);
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

O resultado é apresentado abaixo:

SQL---->INSERT INTO TBPessoa values(str_nome='Nelson',idade='28',str_end='Rua tal')
SQL---->INSERT INTO TBFunc values(str_nome='Glauber',idade='18',str_end='Rua x',num_ctps='1234')

Como podemos observar, o mesmo repositório está persistindo duas classes distintas, baseando-se apenas nas notações que usamos nelas. Esse recurso pode trazer inúmeras vantagens para o desenvolvimento de aplicações facilitando a componentização da sua aplicação.

4br4ç05,
nglauber

PS.: Me baseei nesse post.

segunda-feira, 13 de fevereiro de 2012

Android: Dicas 6

Olá povo,

Mais um post da série "Dicas de Android". Deixem suas sugestões para os próximos posts.

1 Instalar APK a partir da sua aplicação.
File apk = new File(
  Environment.getExternalStorageDirectory() + 
  "/Copas.apk");
Intent it = new Intent(Intent.ACTION_VIEW);
it.setDataAndType(Uri.fromFile(apk),
  "application/vnd.android.package-archive");
startActivity(it);

O código acima, carrega um arquivo (Copas.apk) que está na raiz do cartão de memória e dispara uma Intent que perguntará se o usuário deseja instalar a aplicação. Ao confirmar, a aplicação será instalada.

2. Criando arquivos no cartão de memória.
O código abaixo mostra como criar um diretório no cartão de memória do aparelho e depois cria um arquivo nesse diretório.
File meuDir = new File(
  context.getExternalFilesDir(null), "meuDir");
if (!meuDir.exists()) {
  meuDir.mkdir();
}

try {
 File arquivoTxt = new File(meuDir, "arquivo.txt");
 if (!arquivoTxt.exists()) {
   arquivoTxt.createNewFile();
 }
 FileOutputStream fos =
   new FileOutputStream(arquivoTxt);

 fos.write("Exemplo".getBytes());
 fos.close();
   
} catch (IOException e) {
 e.printStackTrace();
}


Adicione a permissão no AndroidManifest.xml
<uses-permission
 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Vale salientar que o código acima criará um diretório com o nome do pacote da aplicação na pasta Android que está na raiz do cartão de memória. Outro detalhe importante é que o arquivo será excluído caso a aplicação seja desinstalada.

3. Texto sublinhado ou strike no TextView
No editor visual do Android podemos alterar a propriedade android:textStyle para bold ou italic. Mas para o strike (como isso) ou sublinhado (como isso). Precisamos utilizar o código abaixo.
textView.setPaintFlags(
  textView.getPaintFlags() | 
  Paint.STRIKE_THRU_TEXT_FLAG);

O código acima, coloca o textView como strike. Para usar sublinhado, use a constante UNDERLINE_TEXT_FLAG da classe Paint.
Crédito dessa dica para Alvaro Cavalcanti.

4. Voltando para tela inicial da Activity
Digamos que você tem as Activities A, B, C e D. E da tela D você quer voltar pra A, faça o seguinte:
Intent it = new Intent(this, A.class);
it.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(it);

Essa chamada fará com que a Activity "A" seja re-criada, ou seja, o onCreate da mesma será chamado novamente. Para evitar isso, coloque no manifest, na declaração abaixo:
<activity
  android:name=".A"
  android:label="Primeira Activity"
  android:launchMode="singleTop"/>

Com isso, o método onNewIntent será chamado (ao invés do onCreate) e você poderá fazer algum tratamento lá baseado na nova Intent que está chegando.

5. Aumentando a memória disponível para aplicação
Estava trabalhando em uma aplicação Android para tablets que estava dando OutOfMemoryError. Após algumas investigações checamos que estávamos alocando muitas imagens, mas estávamos desalocando todas, ou seja, aparentemente tudo certo. Então nos deparamos com a propriedade abaixo:
<application
  android:largeHeap="true"
  ...>

Sem muita documentação, a propriedade acima, aumenta a memória da Dalvik (máquina virtual do Android) reservada para a aplicação, e para nós, funcionou.
Créditos para Andréa Santos (apesar dela não querer compartilhar informações) :D

6. Evitando cast no findViewById
Todo mundo que fez mais que um HelloWorld com Android, sabe que para obter a referência de um componente declarado em um arquivo de layout utiliza-se o método findViewById. Mas como esse método retorna um objeto da classe View (que é a superclasse de todos os componente visuais do Android) temos que fazer o cast para o componente desejado como abaixo:
Button b = (Button)findViewById(R.id.meuBotao);

Mas existe uma maneira de evitar esse cast utilizando o recurso de Generics do Java. Basta declarar o método abaixo na sua Activity.
@SuppressWarnings("unchecked")
private <T>T getViewById(int id){
  return (T)findViewById(id);
}

E para utilizar:
Button b = getViewById(R.id.meuBotao);

Economizamos aí alguns caracteres :) Créditos para Gustavo Pinto e José André Henrique.

Dúvidas ou sugestões? Deixem seus comentários.

4br4ç05,
nglauber