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

sexta-feira, 3 de fevereiro de 2012

Artigo "Android 4: Ice Cream Sandwich"

Olá povo,

A revista Java Magazine deste mês, traz em sua edição comemorativa de número 100, a primeira de duas matérias sobre a nova versão da plataforma Android. O texto foi escrito por mim e pelo meu colega Bruno Vinícius.

O Ice Cream Sandwich, como é denominada a nova versão, veio para unificar os smartphones e tablets Android. Neste artigo, apresentamos as novas APIs da versão 4.0 do sistema operacional da Google, que vieram para ajudar a encarar de frente e superar seus concorrentes. As várias melhorias de desempenho e diversas novas funcionalidades para os usuários e desenvolvedores, também são descritas no texto.

A segunda parte da matéria deverá sair em março.
Espero que vocês gostem.

4br4ç05,
nglauber

terça-feira, 31 de janeiro de 2012

Fragmentos para todos

Olá povo,

Uma das novidades do Android 3 (Honeycomb), foi a adição do conceito de Fragments. Com essa versão foi escrita exclusivamente para tablets, os "fragmentos" permitem a separação de uma Activity em pedaços de modo a aproveitar os grande tamanho de tela desse tipo de dispositivo. Os Fragments funcionam como sub-activities (dentro de uma Activity), e têm seu próprio ciclo de vida, tratando também os eventos dos componentes nele contidos.

Você pode estar se perguntando: ótimo, mas isso é só pro Android 3 ou superior, então não roda no 2.x. Você estaria certo se a Google não tivesse disponibilizado uma API de compatibilidade, que permite usufruir de alguns recursos do Honeycomb em aparelhos 1.6 (Donut) ou superior. Essa biblioteca está disponível no Android SDK Manager, o mesmo lugar onde você baixa os pacotes necessários para o desenvolvimento Android. Essa API é basicamente um JAR que deve ser adicionado no seu projeto Android.

A Figura abaixo mostra a opção para baixar a API de compatibilidade no Android SDK Manager.

EDITADO EM 10/08/2012
Na versão atual do plugin, essa lib é adicionada automaticamente ao projeto.

Uma vez baixado, o JAR vá até a pasta do SDK e procure pelo arquivo android-support-v4.jar. Ele pode estar na pasta extras/android/compatibility/v4 ou android-compatibility/v4 (dependendo do S.O.).
Crie um novo projeto Android selecionando a versão 1.6 ou superior, e na raiz do projeto, crie uma pasta lib. Copie o JAR para essa pasta e depois adicione-o ao Build-Path do projeto (botão direito sobre JAR, Build Path > Add to build path). Em seguida, clique com o botão direito sobre o projeto e clique em propriedades. Por fim, selecione Java Build Path e na aba Order and Export, marque o android-support-v4.jar.

Antes de começarmos a codificar, vou explicar como será o exemplo. Será uma aplicação clássica de Fragments, onde teremos uma lista, e ao clicar em um dos elementos dessa lista, os detalhes daquele item serão exibidos. Como exemplo, listarei as versões do Android e ao clicar, exibir os detalhes da respectiva versão.

Vou começar criando o layout principal da aplicação. Crie o arquivo res/layout/titles.xml e deixe-o conforme abaixo:
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="horizontal" >

  <fragment
    android:id="@+id/titles"
    android:layout_width="0px"
    android:layout_height="fill_parent" 
    android:layout_weight="1"
    class="ngvl.android.fragments2.TitlesFragment" />

  <FrameLayout
    android:id="@+id/details"
    android:layout_width="0px"
    android:layout_height="fill_parent"
    android:layout_weight="1" />
</LinearLayout>

Aqui temos um LinearLayout horizontal com um Fragment que está apontando para a classe TitlesFragment e está ocupando metade da tela (determinado pela propriedade layout_weight). E um FrameLayout para exibir os detalhes da opção selecionada.

O arquivo acima dará erro porque não criamos a classe TitlesFragment. Ela é mostrada abaixo:
public class TitlesFragment extends ListFragment {

  private final String EXTRA_INDEX = "index";
  private int selectedIndex;
 
  @Override
  public void onActivityCreated(
    Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
  
    ListView lv = getListView();
  
    setListAdapter(
      new ArrayAdapter<String>(
      getActivity(),
      android.R.layout.simple_list_item_checked,
      Dados.titles));
  
    lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
  
    if (savedInstanceState != null){
      selectedIndex = 
        savedInstanceState.getInt(EXTRA_INDEX);
      lv.setSelection(selectedIndex);
    }

    if (isDualPane()){
      showDetails(selectedIndex);
    } else {
      getActivity().findViewById(
        R.id.details).setVisibility(View.GONE);
    }
  
    lv.setItemChecked(selectedIndex, true);  
  }

  private void showDetails(int index) {
    selectedIndex = index;
  
    if (isDualPane()) {  
      DetailsFragment details = 
        (DetailsFragment)getFragmentManager()
          .findFragmentById(R.id.details);
   
      if (details == null || 
        details.getShowIndex() != index){

        details = DetailsFragment.newInstance(index);
    
        FragmentTransaction fragTrans =
          getFragmentManager().beginTransaction();
        fragTrans.replace(R.id.details, details);
        fragTrans.setTransition(
          FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        fragTrans.commit();
      }
    } else {
      Intent it = new Intent(getActivity(), 
        DetailsActivity.class);

      it.putExtra(EXTRA_INDEX, index);
      startActivity(it);
    }
  }
 
  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(EXTRA_INDEX, selectedIndex);
  }
 
  @Override
  public void onListItemClick(ListView l, 
    View v, int position, long id) {
    super.onListItemClick(l, v, position, id);
    showDetails(position);
  }
 
  private boolean isDualPane(){
    Configuration config =
      getActivity().getResources().getConfiguration();

    return config.orientation == 
      Configuration.ORIENTATION_LANDSCAPE;
  }
}

Nosso fragmento herda de ListFragment, que é bem similar a classe ListActivity, e exibe informações em formato de lista. Então, no método onActivityCreated (o nome por sí só já diz o que ele faz :) obtemos a referência para a ListView e setamos um Adapter para ela. Os dados que são passados para esse Adapter vêm de um array de Strings contendo as versões do Android definidos na classe Dados. Parte dessa classe é mostrada abaixo:
public class Dados {
  static public String[] titles = {
    "Android 1.5",
    "Android 1.6",
    // Mais itens aqui
  }

  static public String[] description = {
    "Primeira versão do Android, Cupcake",
    "Donut já suporta Fragments",
    // Mais descrições aqui
  }
}

Em seguida, verificamos se existe um estado salvo da nossa Activity. Quando entramos a primeira vez, esse estado estará vazio, mas quando girarmos o aparelho, o método onSaveInstance será chamado e nós salvaremos a posição da lista que está checada. Se já existir um estado saldo, marcamos a opção que estava selecionada antes de girar o aparelho e fazemos o scroll até ela.
Logo após, verificamos se a aplicação é dualPane (dois painéis) esse método (que está no fim da classe) verifica se a orientação do aparelho está como landscape. Em caso positivo exibiremos a lista e os detalhes no mesmo momento, caso contrário exibiremos apenas a lista, então ocultamos o FrameLayout de detalhes.
O método showDetails verifica se a tela é dualPane, em caso positivo, instancia o fragmento de detalhes e o exibe utilizando a class FragmentTransition. Caso não seja dualPane, ou seja, está em Portrait, exibimos a Activity de detalhes.

Com a utilização de Fragments, a Activity passa a controlar o Fragment, e o Fragment trata eventos de UI. Com isso a Activity fica bem simples como podemos ver abaixo:
public class TitleActivity extends FragmentActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.titles);
  }
}

O único detalhe é que essa clase herda de FragmentActivity. Lembrando que ela é uma classe da API de compatibilidade, no Android 3.x podemos continuar usando Activity mesmo. Agora vamos analisar o fragmento de detalhes
public class DetailsFragment extends Fragment {

  private static final String EXTRA_INDEX = "index";
 
  public static DetailsFragment newInstance(int index) {
    DetailsFragment fragment = new DetailsFragment();
  
    Bundle params = new Bundle();
    params.putInt(EXTRA_INDEX, index);
    fragment.setArguments(params);
  
    return fragment;
  }
 
  public int getShowIndex() {
    return getArguments().getInt(EXTRA_INDEX);
  }

  @Override
  public View onCreateView(LayoutInflater inflater,
    ViewGroup container, Bundle savedInstanceState) {
  
    TextView txt = new TextView(getActivity());
    txt.setText(Dados.description[getShowIndex()]);
  
    return txt;
  }
}

A classe tem um Factory Method (chamado newInstance) que cria um DetailsFragment já passando os "extras" necessários. O método onCreateView define o que será exibido no Fragment, para simplificar, criamos apenas um TextView, mas você poderia usar o parâmetro inflater para carregar um arquivo de Layout que desejasse.
Por fim, vamos mostrar a Activity de detalhes.
public class DetailsActivity extends FragmentActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  
    if (isDualPane()){
      finish();
      return;
   
    } else if (savedInstanceState == null) {
  
      DetailsFragment details = new DetailsFragment();
      details.setArguments(getIntent().getExtras());
   
      getSupportFragmentManager()
        .beginTransaction()
        .add(android.R.id.content, details)
        .commit();
    }
  }
 
  private boolean isDualPane(){
    Configuration config =
      getResources().getConfiguration();

    return config.orientation ==
      Configuration.ORIENTATION_LANDSCAPE;
  }
}

Nessa Activity, basicamente verificamos se a orientação é landscape, em caso positivo, essa Activity é fechada, e consequentemente a TitlesActivity é exibida mostrando os títulos e os detalhes do mesmo. Já se Activity não é dual pane (ou seja, é portrait) e foi passado o índice para ela, carregamos o Fragment e o adicionamos à tela.

A imagem abaixo mostra nossa aplicação rodando em landscape.

Esse artigo foi baseado nesse post do Blog do Android Developers.

Qualquer dúvida, deixem seus comentários,

4br4ç05,
nglauber

sábado, 14 de janeiro de 2012

Android + iOS = Adobe Air

Olá povo,

Atualmente a disputa entre as plataformas para dispositivos móveis está bastante acirrada. Android e iOS travam uma verdadeira batalha pela preferência dos usuários, tanto no mercado de tablets quanto de smartphones. E nós, como desenvolvedores, temos de estar atentos a essa peleja.
Imagine que você recebe uma demanda de um projeto em que um dos requisitos, é rodar em iOS e Android. Nesse caso, o primeiro pensamento normalmente é: "putz! vou ter que fazer duas aplicações?".
Quem me conhece, sabe que meu projeto de mestrado foi o desenvolvimento de uma ferramenta que traduz aplicações iOS para Android. Mas infelizmente ela ainda está longe de ficar pronta :( Mesmo assim, durante as pesquisas do mestrado, analisei algumas ferramentas "concorrentes" da minha como o PhoneGap e Titanium. Entretanto, dentre todas, a que gostei mais foi o Adobe Air. Entre os pontos positivos da tecnologia estão: a produtividade, utilizando uma ferramenta RAD com diversos recursos; a linguagem de programação é dinâmica e bastante fácil de aprender; e o aspecto visual que se mantém igual no Android e no iOS.

A aplicação é desenvolvida em Flex e/ou ActionScrpit com o Flash Builder (ou Flash Professional CS5). Nesse post, vou mostrar como iniciar o desenvolvimento e fazer o bom e velho "Hello World". Para começar, você precisa fazer o download do Flash Builder 4.6 no site da Adobe. Ele é uma IDE baseada em Eclipse, onde você pode baixar um trial de 60 dias, e após esse tempo deve ser adquirido. Faça o download e instale o Flash Builder Trial no seu computador e vamos começar a brincadeira :)

Acesse o menu File > New > Flex Mobile Project. Na janela exibida abaixo, digite o nome do projeto e o local onde o projeto será salvo e clique em Next.

Na próxima janela do assistente, selecionamos para quais plataformas iremos desenvolver: iOS, Android e/ou BlackBerry. Deixei aqui Android e iOS. Na aba "Application Template" selecionamos o formato de aplicação que devemos desenvolver, aqui deixei uma aplicação Blank. As outras opções permitem criar aplicações que naveguem entre telas ou com abas respectivamente.
Na aba "Permission", selecionamos as permissões necessárias para acessar recursos de aplicações Android (para iOS ela não é usada). E em "Platform Settings", selecionamos para quais aparelhos iOS iremos desenvolver (iPad, iPod, iPhone).
Na seção "Application settings", marcamos se a aplicação deve mudar de orientação (portrait e landscape) automaticamente; se será tela cheia; e se a aplicação será esticada para se ajustar a telas de diferentes densidades. Deixe as configurações como abaixo e clique em Next.

Na próxima tela poderíamos selecionar se estamos utilizando alguma tecnologia servidor. Mas como não é o caso, clique em Next.
Na última tela do assistente, vamos deixar as opções padrão, exceto pelo campo Application ID, onde preencheremos com o esquema similar ao que usamos em Java. ex:"ngvl.air.exemploblog". Outro campo interessante dessa tela, é o "Main Application File" onde definimos o arquivo que será o ponto de partida da nossa aplicação. Aqui, deixamos HelloWorldBlog.mxml e clicamos em Finish.

Ao iniciar o projeto, o arquivo HelloWorldBlog.mxml é aberto em modo texto, mas vamos primeiro edita-lo no modo design. Para tal, clique no botão na parte superior do editor de código, e no combo "Device" selecione um aparelho de sua preferência.

Na paleta de componentes, localizada do lado esquerdo, arraste para a tela de design: um Label, um TextInput e um Button. De um duplo-clique nos componentes Label e Button para alterar o texto. E deixe conforme abaixo:

Agora vá para o modo Source e deixe conforme abaixo;
<?xml version="1.0" encoding="utf-8"?>
<s:Application
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
applicationDPI="160">

<fx:Script>
<![CDATA[
protected function botaoFilhaoClick(
event:MouseEvent):void
{
lblTexto.text = edtTexto.text;
}
]]>
</fx:Script>
<fx:Declarations>
</fx:Declarations>
<s:Label
id="lblTexto"
x="39" y="48"
text="Olá mundo Adobe Air"/>
<s:TextInput
id="edtTexto"
x="39" y="71"/>
<s:Button
x="39" y="112"
label="Vai filhão!"
click="botaoFilhaoClick(event)"/>
</s:Application>

Em seguida, clique com o botão direito sobre o projeto e selecione Run As... > Mobile Application. Aparecerá a janela abaixo. Selecione a plataforma que deseja testar (aqui usei Android) e em Launch Method você pode escolher entre rodar em um simulador ou em um dispositivo real. Aqui, selecionei para rodar no Desktop, e então o emulador com o tamanho de tela do Motorola Defy +. Clique em Run.

A figura abaixo mostra nossa aplicação em execução no simulador do Adobe Air. Ao clicar no botão, o texto digitado na caixa de texto é exibido no Label.


Bem povo, esse é apenas um pontapé inicial. Espero colocar mais posts sobre Adobe Air aqui no blog. Aproveito para agradecer ao meu grande amigo Eric Cavalcanti(@ericoc), com quem tive aulas de Adobe Air e me ajudou a iniciar nessa tecnologia.

Dúvidas? Deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 2 de janeiro de 2012

Widgets + Service

Olá povo,

Eu já coloquei aqui no blog dois posts sobre widgets (veja aqui e aqui), mas para quem não os viu, um AppWidget é uma aplicação que roda na HomeScreen dos aparelhos Android. E um dos grandes problemas ao se desenvolver esse tipo de aplicação é que o tratamento dos eventos que são disparados por ele. Eles podem ser feitos com uma subclasse de AppWidgetProvider (que herda de BroadcastReceiver), entretanto eles não mantêm estado. Por ser um BroadcastReceiver, o seu programa só interage com ele durante a execução do método onReceive.

Alguns colegas de trabalho (que não vou citar nomes :) usam classes com atributos estáticos para manter os estado dos AppWidgets. Mas digamos que essa não é a maneira mais elegante :) Por isso, hoje vou mostrar como integrá-los com o componente Service do Android.

O componente Service permite a sua execução em segundo plano mesmo que não haja nenhuma tela da aplicação aberta. Seu ciclo de vida inicia quando é disparada uma Intent para a classe do serviço utilizando o método startService. Caso o serviço ainda não esteja sendo executado, o mesmo será iniciado e ficará ativo até que o método stopService seja chamado ou que o próprio serviço chame o método stopSelf.

Tendo em vista essa característica do serviço, vou utilizá-lo nesse exemplo para guardar o estado do widget, que ficará fazendo chamadas ao serviço para obter informações e se atualizar. Nosso exemplo será um widget que mostrará alguns links favoritos que nós poderemos passar clicando nos botões de navegação.

Vamos começar pelo código do Service, que está na classe MeuServico

public class MeuServico extends Service {

// Sites favoritos
private String[] sites = {
"nglauber.blogspot.com",
"developer.android.com",
"www.unibratec.edu.br",
"www.especializa.com.br",
"www.cesar.edu.br" };

// índice do site que deve ser mostrado no widget
private int indice;

@Override
public int onStartCommand(Intent intent, int flags,
int startId) {
// O Widget vai chamar esse método cada vez que
// clicarmos no botão
// Obtendo o botão clicado (próx. ou ant.)
String acao = intent.getStringExtra(
MeuWidget.EXTRA_ACAO);

// Se foi próximo, incrementa o índice
if (MeuWidget.ACAO_PROXIMO.equals(acao)) {
indice++;

if (indice > sites.length - 1) {
indice = 0;
}

// Se foi anterior, decrementa o índice
}else if(MeuWidget.ACAO_ANTERIOR.equals(acao)){
indice--;
if (indice < 0) {
indice = sites.length - 1;
}
}
// Se veio alguma ação, atualize o widget
if (acao != null) {
RemoteViews views = new RemoteViews(
this.getPackageName(), R.layout.main);

// Atualizando TextView pra exibir o site
views.setTextViewText(
R.id.txtSite, sites[indice]);

// ID do widget que deve ser atualizado
int appWidgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);

// Obtendo a instância do AppWidgetManager
// para atualizar o Widget
AppWidgetManager appWidgetManager =
AppWidgetManager.getInstance(this);

// atualiza o widget
appWidgetManager.updateAppWidget(
appWidgetId, views);
}

return super.onStartCommand(
intent, flags, startId);
}

@Override
public IBinder onBind(Intent arg0) {
// Não usado aqui
return null;
}
}

O código acima, cria um serviço que tem uma lista de links favoritos, e um índice que controlará qual link deve ser exibido. O widget então fará requisições ao serviço, que serão tratadas pelo método onStartCommand, que mostrará um link da lista baseado na posição do array.
É importante ter em mente que o widget não roda no processo da nossa aplicação, e sim da HomeScreen, então para ter acesso aos componentes gráficos usamos a classe RemoteViews e atualizamos o seu conteúdo com a classe AppWidgetManager.

Uma vez que o Service está pronto, vamos começar a criar o widget. Assim como uma Activity, ele terá um arquivo de layout. Utilizei o próprio res/layout/main.xml que é criado com o projeto. Ele ficou conforme abaixo:

<?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"
android:background="#000000"
android:layout_margin="20dp">
<Button
android:id="@+id/btnAnterior"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text="<" />
<TextView
android:id="@+id/txtSite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:layout_weight="1"
android:text="Clique para navegar nos favoritos"/>
<Button
android:id="@+id/btnProximo"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text=">" />
</LinearLayout>

Nada de especial no Layout acima, então vamos ver como fica o código do Widget.

public class MeuWidget extends AppWidgetProvider {
public static final String EXTRA_ACAO = "acao";
public static final String ACAO_ANTERIOR="anterior";
public static final String ACAO_PROXIMO = "proximo";

@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager,
int[] appWidgetIds) {

super.onUpdate(context,
appWidgetManager, appWidgetIds);

// As view estão na aplicação Home e não na
// nossa. Por isso, para obter as referências
// dos componentes é usada a classe RemoteViews
RemoteViews views = new RemoteViews(
context.getPackageName(), R.layout.main);

// Esse método recebe a lista dos widgets que
// devem ser atualizados, pois podem haver várias
// instâncias do mesmo na Home.
// Então devemos atualizar todos.
for (int i = 0; i < appWidgetIds.length; i++){
// Configurando evento dos botões
setButtonClicks(context, appWidgetIds[i], views);
}
appWidgetManager.updateAppWidget(
appWidgetIds, views);
}

private void setButtonClicks(
Context context, int appWidgetId,
RemoteViews views) {
// Os eventos de click só podem disparar
// PendingIntents. No nosso caso,
// dispararemos PendingIntents para o nosso
// serviço. Para setar o evento, passa o id
// do componentes e a PendingIntent
views.setOnClickPendingIntent(
R.id.btnProximo,
servicePendingIntent(
context, ACAO_PROXIMO, appWidgetId));

views.setOnClickPendingIntent(
R.id.btnAnterior,
servicePendingIntent(
context, ACAO_ANTERIOR, appWidgetId));
}

private PendingIntent servicePendingIntent(
Context ctx, String acao, int appWidgetId){

// Criando a Intent para chamar o serviço
Intent serviceIntent = new Intent(
ctx, MeuServico.class);

// Passando a ação (proximo ou anterior)
serviceIntent.putExtra(EXTRA_ACAO, acao);

// Passando o ID do widget que clicou
serviceIntent.putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetId);

// O requestId da PendingIntent deve ser único,
// então gero um número aleatório
int requestId = new Random().nextInt();

// criando a PendingIntent para o serviço
PendingIntent pit = PendingIntent.getService(
ctx, requestId, serviceIntent, 0);
return pit;
}
}

No código acima, o método onUpdate é chamado quando o widget é adicionado à tela. Note que a referência aos componentes é feita através da classe RemoteViews, este é outro conceito que não deve ser esquecido: o widget roda na aplicação Home, logo, essas Views lá. Outro detalhe interessante é que os componentes só podem disparar PendingIntents ao serem clicados. E no nosso caso, essas PendingIntents estão chamando o Service, mudando apenas os parâmetros que são passados (próximo e anterior).

Agora vamos criar o arquivo de configuração do widget. Na pasta res, crie a pasta xml (minúsculo) e adicione o arquivo meuwidget.xml e coloque o conteúdo abaixo:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/main"
android:minHeight="72dp"
android:minWidth="294dp" >
</appwidget-provider>

Assim como todo BroadcastReceiver ele deve ser declarado no AndroidManifest.xml, só que ele deve tratar a ação APPWIDGET_UPDATE e deve ter uma tag meta-data referenciando o arquivo de configuração que criamos anteriormente.

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.edu.cesar.aula12"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:minSdkVersion="10" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >

<receiver
android:name="MeuWidget"
android:label="Meu Widget">
<!-- IntentFilter obrigatória
para todo widget -->
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<!-- Arquivo de configuração do widget -->
<meta-data android:resource="@xml/meuwidget"
android:name="android.appwidget.provider"/>
</receiver>

<!-- Meu Serviço -->
<service android:name="MeuServico"/>
</application>
</manifest>

Mande rodar a aplicação. Nada irá aparecer, pois você não tem nenhuma Activity no projeto. Para ver se o Widget está funcionando, dê um clique-longo na Home ou clique na tecla "Menu" e em seguida, selecione "Add". No pop-up que for exibido, selecione "Widgets", e depois "Meu Widget". A mensagem inicial do widget será exibida, depois é clicar nos botões para ver o resultado.

Como vocês observaram dexei o código comentado, mas quem tiver dúvidas, deixem seus comentários.

4br4ç0s,
nglauber