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.