domingo, 30 de dezembro de 2012

Android Google Maps v2

Olá povo,

No último dia 03/12/2012  a Google disponibilizou a versão 2.0 da API do Google Maps para Android que trouxe diversas melhorias em relação a versão anterior. O problema é que tudo mudou completamente :) Desde a geração da chave de acesso até as classes envolvidas. A parte boa disso tudo, é que essa API funciona desde a versão 2.2 do Android e trouxe melhorias significativas \o/
Nesse post vou mostrar como dar os primeiros passos nessa nova API.

Gerando a chave de acesso

Quem já fez algum exemplo sabe que precisamos de uma chave de acesso para utilizar o serviço de mapas. Para ter acesso aos mais diversos serviços do Google, precisamos criar um projeto no site do Google APIs Console. Uma vez que o projeto foi criado, acesse a opção Services e habilite o serviço Google Maps Android API v2. Uma vez habilitado o serviço, acesse a opção API access depois clique no botão Create new Android key, no popup que for exibido clique em Create.
Será gerada uma chave no padrão SHA-1 que usaremos no nosso código similar a essa: AIzbSyCyoMobvh72ZrSv4xQddOLzDOlaLqCcILU

Biblioteca dos serviços do Google Play
A segunda etapa é baixar o pacote de serviços do Google Play. Abra o Android SDK Manager e baixe o Google Play services.
Quando terminar o download, será criado o seguinte diretório [android-sdk/extras/google/google_play_services/libproject] que contém as classes necessárias para usar a nova API. Precisamos importar esse projeto para dentro do Eclipse, pois nosso projeto irá referencia-lo.
Acesse a opção File | New project... Em seguida, selecione Android | Android Project From Existing Code e então selecione o diretório citado anteriormente. O projeto será importado para o Eclipse e agora podemos começar nosso projeto de exemplo.

Projeto de exemplo
Crie um novo projeto Android e vamos começar a brincadeira :) A primeira coisa é referenciar o projeto  do Google Play Service dentro do nosso projeto. Para isso, clique com o botão direito sobre o projeto e selecione  Properties. Na janela que for exibida, selecione Android do lado esquerdo. Clique no botão Add... e selecione google-play-services_lib e então clique em Ok.
Estamos prontos pra começar, deixe o AndroidManifest.xml como abaixo.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=
  "http://schemas.android.com/apk/res/android"
  package="ngvl.testegmaps_v2"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-sdk
    android:minSdkVersion="9"
    android:targetSdkVersion="15" />

  <uses-permission android:name=
    "android.permission.INTERNET" />
  <uses-permission android:name=
    "android.permission.ACCESS_NETWORK_STATE"/>
  <uses-permission android:name=
    "android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name=
    "com.google.android.providers.gsf.permission.READ_GSERVICES" />

  <uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

  <application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
      android:name="ngvl.testegmaps_v2.MainActivity"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name=
          "android.intent.action.MAIN" />
        <category android:name=
          "android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <meta-data android:name=
      "com.google.android.maps.v2.API_KEY"
      android:value="suaApiKeyAqui!!!!!!" />
  </application>
</manifest>

Vamos comentar alguns detalhes do arquivo. Começamos utilizando as permissões de INTERNET e WRITE_EXTERNAL_STORAGE para podermos baixar os mapas e fazer cache dos mesmos no cartão de memória. Um requisito dessa nova API é que o aparelho deve suportar OpenGL que declaramos logo em seguida. A última configuração importante que fizemos aqui é a tag meta-data que declaramos a chave do Google Maps que geramos na primeira etapa desse post.
Agora deixe o arquivo de layout conforme abaixo.
<fragment
  xmlns:android=
    "http://schemas.android.com/apk/res/android"
  android:id="@+id/map"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:name=
    "com.google.android.gms.maps.SupportMapFragment" />
Aqui temos uma grande mudança: a classe MapView e MapActivity não são mais utilizadas. Em seu lugar entrou a classe MapFragment, mas como quis deixar a compatibilidade com versões anteriores do Android, usei a SupportMapFragment. Isso nos permite utilizar mapas em fragments, o que só era possível anteriormente através de APIs de terceiros, uma vez que tínhamos que herdar de MapActivity. Outro detalhe é que o nome do pacote também mudou, agora é com.google.android.gms.maps.
Por fim, vamos ao código da Activity.

package ngvl.testegmaps_v2;

import android.os.Bundle;
import android.support.v4.app.*;

import com.google.android.gms.common.*;
import com.google.android.gms.maps.*;
import com.google.android.gms.maps.model.*;

public class MainActivity extends FragmentActivity {

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    SupportMapFragment fragment =
     (SupportMapFragment)getSupportFragmentManager()
       .findFragmentById(R.id.map);
    GoogleMap map = fragment.getMap();

    LatLng latLng = new LatLng(-23.561706,-46.655981);
    map.addMarker(new MarkerOptions()
      .position(latLng)
      .icon(BitmapDescriptorFactory.fromResource(
        R.drawable.ic_launcher))
      .title("Av. Paulista")
      .snippet("São Paulo"));
  
    configuraPosicao(map, latLng);
  }

  private void configuraPosicao(
    GoogleMap map, LatLng latLng) {

    map.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
    map.animateCamera(
     CameraUpdateFactory.newLatLngZoom(latLng, 17.0f));
  }
}
Nossa classe herda de FragmentActivity da API de compatibilidade do Google que falei aqui. Em seguida, obtemos a referência para o MapFragment, e a partir dele obtemos a instância do GoogleMaps (que substitui o MapView). A classe GeoPoint foi substituída pela classe LatLng que representa uma coordenada geográfica. Com essa informação, adicionamos um ponto no mapa, passando uma imagem que servirá de marcador no mapa, um título e um resumo, que serão exibidos ao clicar na imagem no mapa. O método configuraPosicao, define o tipo de mapa para satélite e o zoom para 17 (os valores variam de 2 a 21, sendo 21 mais próximo possível).
Se rodarmos nossa aplicação, em um aparelho 2.3 ficará similar a figura abaixo:
Uma das novidades dessa versão da API do GoogleMaps é poder visualizar o mapa em relevo, mostrando, por exemplo, os prédios do local selecionado no mapa. Modifique o método configuraPosicao para ficar como abaixo:
private void configuraPosicao(
  GoogleMap map, LatLng latLng) {
  map.moveCamera(
   CameraUpdateFactory.newLatLngZoom(latLng, 15));
  map.animateCamera(
   CameraUpdateFactory.zoomTo(10), 2000, null);

  CameraPosition cameraPosition = 
    new CameraPosition.Builder()
      .target(latLng)   
      .zoom(17)     
      .bearing(90)
      .tilt(45)
      .build();
  map.animateCamera(
    CameraUpdateFactory.newCameraPosition(
      cameraPosition));
}
Notem que não usamos o modo satélite aqui (não ficou muito bom quando usei :) mas posicionamos a câmera de uma maneira diferente. Além da Latitude/Longitude e do Zoom, setamos o bearing (rotação do mapa em 90 graus) e o tilt (inclinação em 45 graus). O resultado pode ser visto abaixo.
Bem pessoal, espero que tenham gostado e qualquer dúvida, deixem seus comentários.
Mais detalhes em: https://developers.google.com/maps/documentation/android/

 4br4ç05,
nglauber

quarta-feira, 26 de dezembro de 2012

Android ExpadableListView

Olá povo,

Esse post estava no rascunho a algum tempo e resolvi publica-lo finalmente :) Vou mostrar como utilizar a lista agrupada do Android, a ExpandableListView. Como o próprio nome diz, ela contém uma lista de elementos que ao serem clicados exibem uma sublista. Como exemplo, teremos uma lista de estados, e ao clicar no nome do estado, exibiremos suas respectivas cidades.
Abaixo temos o código da Activity da aplicação.

public class ExemploExpladableListViewActivity
  extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(
      R.layout.activity_exemplo_expladable_list_view);

    ExpandableListView listview =(ExpandableListView)
      findViewById(R.id.expandableListView1);
   
    List<String> listPe = new ArrayList<String>();
    listPe.add("Caruaru");
    listPe.add("Recife");
   
    List<String> listSp = new ArrayList<String>();
    listSp.add("São Paulo");
    listSp.add("Campinas");
   
    Map<String, List<String>> dados = 
      new HashMap<String, List<String>>();
    dados.put("PE", listPe);
    dados.put("SP", listSp);
   
    listview.setAdapter(
      new MeuExpadableAdapter(dados));
  }
}

Como podemos notar, ao invés de usar uma lista simples, utilizamos um HashMap onde cada elemento será composto de uma chave que será o nome do estado e o valor será uma lista com as cidade daquele estado. O arquivo de layout contém apenas uma ExpandableListView, conforme abaixo.

<ExpandableListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/expandableListView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Como uma ListView comum, utilizaremos um Adapter para exibir o conteúdo da lista. A diferença é que herdaremos de BaseExpandableListAdapter, onde teremos que implementar mais alguns métodos além dos que já estamos acostumados nas listas convencionais:

  • getGroup: retorna o objeto que representa o grupo de informações. No nosso caso, a sigla do estado;
  • getGroupCount: retorna quantos grupos nós teremos. Aqui estamos retornando quantas entradas (chaves) temos na nossa HashMap;
  • getGroupId: algum inteiro que sirva de identificador único para o grupo, aqui estamos retornando a posição do grupo;
  • getGroupView: retorna a View que representará visualmente o grupo na lista;
  • getChild: similar ao getItem do Adapter convencional, a diferença é que é passado o índice do grupo e do elemento que se deve retornar;
  • getChildId: identificador único do elemento do grupo;
  • getChildView: retorna a View que representará o elemento dentro do grupo. Similar ao getView do adapter convencional;
  • getChildrenCount: retorna a quantidade de elementos dentro de um grupo;
  • hasStableIds: informa se os identificadores dos grupos e dos elementos são fixos;
  • isChildSelectable: retorna true se os elementos filhos podem ser selecionados, ou false caso contrário.

class MeuExpadableAdapter 
  extends BaseExpandableListAdapter {

  private Map<String, List<String>> dados; 
  private List<String> keys;
 
  public MeuExpadableAdapter(
    Map<String, List<String>> dados){

    this.dados = dados;
    this.keys = new ArrayList<String>(
      dados.keySet());
  }
 
  @Override
  public Object getGroup(int groupPosition) {
    return keys.get(groupPosition);
  }

  @Override
  public int getGroupCount() {
    return dados.size();
  }

  @Override
  public long getGroupId(int groupPosition) {
    return groupPosition;
  }

  @Override
  public View getGroupView(int groupPosition, 
    boolean isExpanded, View convertView, 
    ViewGroup parent) {

    View v = LayoutInflater.from(
      parent.getContext()).inflate(
        android.R.layout.simple_expandable_list_item_1, 
        null);

    TextView txt = (TextView)
      v.findViewById(android.R.id.text1);

    txt.setText(keys.get(groupPosition));
    return v;
  }
 
  @Override
  public Object getChild(
    int groupPosition, int childPosition) {

    return dados.get(
      keys.get(groupPosition)).get(childPosition);
  }

  @Override
  public long getChildId(
    int groupPosition, int childPosition) {
    return 0;
  }

  @Override
  public View getChildView(int groupPosition, 
    int childPosition, boolean isLastChild, 
    View convertView, ViewGroup parent) {

    View v = LayoutInflater.from(
      parent.getContext()).inflate(
        android.R.layout.simple_list_item_1, null);
    TextView txt = (TextView)
      v.findViewById(android.R.id.text1);
    txt.setText(dados.get(
      keys.get(groupPosition)).get(childPosition));
    return v;
  }
 
  @Override
  public int getChildrenCount(int groupPosition) {
    return dados.get(keys.get(groupPosition)).size();
  }

  @Override
  public boolean hasStableIds() {
   return true;
  }

  @Override
  public boolean isChildSelectable(
    int groupPosition, int childPosition) {
    return true;
  } 
}

O exemplo acima (com uma leve modificação :) está aqui.
Qualquer dúvida deixem seus comentários.

4br4ç05,
nglauber

quarta-feira, 5 de dezembro de 2012

Curso de iOS à distância

Olá povo,

O CESAR.edu está promovendo o curso à distância de Desenvolvimento de Aplicativos para iOS. As aulas serão ministradas por mim on line e ao vivo, o que permitirá maior interação com os alunos.

A plataforma iOS é uma das líderes no mercado mobile e a demanda por aplicativos para seus dispositivos vem crescendo, e consequente a busca por profissionais capacitados nessa plataforma.
Neste curso serão apresentados os conceitos fundamentais para desenvolvimento de aplicações para os dispositivos móveis da Apple: iPhone, iPad e iPod Touch. O Ambiente de desenvolvimento, a linguagem de programação Objective-C, os componentes de interface gráfica e os principais recursos destes dispositivos, serão demonstrados através de uma abordagem totalmente prática e focadas em problemas reais.

O objetivo do curso é preparar os alunos para desenvolver aplicativos para a plataforma iOS da Apple. Apresentando os principais conceitos, os alunos terão uma base sólida para criar aplicações robustas e que atendam as principais demandas do mercado. Os tópicos apresentados no curso focam em problemas reais, cobrindo os requisitos mais solicitados por aplicações desenvolvidas no mercado.

Aguardo vocês lá!

4br4ç05,
nglauber

segunda-feira, 19 de novembro de 2012

Participação na FEECBR

Olá povo,

Tive a satisfação de ser convidado para palestrar na FEECBR, uma convenção para profissionais de internet de diversas áreas. O evento foi idealizado por um grupo experiente, reconhecido pela qualidade e grau de inovação de projetos educacionais no Nordeste do Brasil. Na oportunidade, palestrantes regionais, nacionais e até internacionais estarão discutindo temas relevantes sobre tecnologia em vários segmentos.
Estarei falando sobre a criação de interfaces inteligentes para smartphones e tablets Android, mostrando os principais padrões e técnicas para obter uma usabilidade atraente, eficiente e seguindo os padrões adotados por aplicações de sucesso.

Editado em 02/12/2012
Segue abaixo os slides da minha palestra.

4br4ç05,
nglauber

segunda-feira, 5 de novembro de 2012

ActionBar no Android 2.x

Olá povo,

Quem me conhece saber que não sou muito fã de frameworks e bibliotecas, gosto de procurar ao máximo uma solução nativa da plataforma ao qual estou trabalhando. E uma coisa que me deixava muito chateado no Android era a não retrocompatibilidade da ActionBar com versões anteriores à 3.0 (quando ela foi lançada).
Felizmente esse problema acabou com a biblioteca SherlockActionBar, que traz o recurso da ActionBar para aparelhos com Android 2.3 ou inferior, e mantém os nomes de métodos iguais à plataforma nativa. E o que é melhor, quando a aplicação está rodando em versões 3.0 ou superior, o Sherlock só faz chamar a API padrão, ou seja, utiliza a ActionBar nativa.

Vou fazer um exemplo que utiliza dois recursos da ActionBar: Menus e Abas. Faça o download em http://actionbarsherlock.com e descompacte em algum local do seu computador. No Eclipse, importe o projeto library para dentro do seu workspace, para tal acesse File/New.../Project, e na janela que for exibida selecione Android/Android Project From existing code selecione o diretório onde você descompactou o Sherlock. Aparecerá além do projeto library, os demos do Sherlock, por agora só importe o projeto library.

Crie um novo projeto selecionando o Build SDK 4.1 (o Sherlock utiliza Temas e Estilos dessa versão do Android) e o Minimum Required para 2.2. Depois de criado, vá até a pasta lib do projeto e apague o arquivo android-support-v4.jar (o Sherlock já inclui esse arquivo). Agora adicione a referência ao projeto library do Sherlock, para tal, clique com o botão direito no projeto e selecione Properties.

Selecione Android, e na parte inferior direita, clique em Add... e selecione o projeto library e clique em OK. Agora, deixe a Activity do seu projeto conforme abaixo:

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.actionbarsherlock.app.*;
import com.actionbarsherlock.app.ActionBar.*;
import com.actionbarsherlock.view.Menu;

public class ExemploSherlockActivity 
  extends SherlockFragmentActivity 
  implements TabListener {

  private Fragmento1 f1;
  private Fragmento2 f2;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTheme(R.style.Theme_Sherlock_Light);
    setContentView(R.layout.activity_exemplo_sherlock);

    f1 = (Fragmento1)getSupportFragmentManager()
         .findFragmentById(R.id.fragmento1);
    f2 = (Fragmento2)getSupportFragmentManager()
         .findFragmentById(R.id.fragmento2);
        
    getSupportActionBar().setNavigationMode(
      ActionBar.NAVIGATION_MODE_TABS);
        
    Tab aba1 = getSupportActionBar().newTab()
               .setText("Aba 1")
               .setTabListener(this);

    Tab aba2 = getSupportActionBar().newTab()
               .setText("Aba 2")
               .setTabListener(this);        
        
    getSupportActionBar().addTab(aba1);
    getSupportActionBar().addTab(aba2);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getSupportMenuInflater().inflate(
      R.menu.activity_exemplo_sherlock, menu);
    return super.onCreateOptionsMenu(menu);
  }

  @Override
  public void onTabSelected(Tab tab, 
    FragmentTransaction ft) {

    if (tab.getPosition() == 0){
      ft.show(f1).hide(f2);
    } else {
      ft.show(f2).hide(f1);
    }
  }

  @Override
  public void onTabUnselected(
    Tab tab, FragmentTransaction ft) {}

  @Override
  public void onTabReselected(
    Tab tab, FragmentTransaction ft) {}
    
  public static class Fragmento1 extends Fragment {
    @Override
    public View onCreateView(LayoutInflater infltr, 
      ViewGroup container, Bundle savedState){

      TextView txt = new TextView(getActivity()); 
      txt.setText("Fragmento 1");
      return txt;
    }
  }
 
  public static class Fragmento2 extends Fragment {
    @Override
    public View onCreateView(LayoutInflater infltr, 
      ViewGroup container, Bundle savedState){

      TextView txt = new TextView(getActivity());
      txt.setText("Fragmento 2");
      return txt;
    }
  }
}
Como estamos trabalhando com Fragment, nossa classe herda de SherlockFragmentActivity. Implementamos a interface TabChangeListener para detectarmos quando houver a alternância entre abas. Declaramos dois fragmentos que serão nas abas, essa classes estão declaradas no final do arquivo, obviamente o ideal é que cada fragmento fique em um arquivo separado.
No onCreate setamos o tema do Sherlock, e em seguida setamos o arquivo de layout da Activity (que está logo abaixo). Depois inicializamos os fragmentos e definimos a forma de navegação da ActionBar para o modo de abas. E por fim, criamos e adicionamos as abas na ActionBar.

Abaixo temos o arquivo de layout da aplicação (res/layout/activity_exemplo_sherlock.xml):
<FrameLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/FrameLayout1"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <fragment
    android:id="@+id/fragmento1"
    android:name="ngvl.android.exemplosherlock.ExemploSherlockActivity$Fragmento1"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

  <fragment
    android:id="@+id/fragmento2"
    android:name="ngvl.android.exemplosherlock.ExemploSherlockActivity$Fragmento2"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</FrameLayout>

Note que o nome da classe é composto do pacote da aplicação + o nome da classe. Como estamos usando uma Inner Class, o sinal de $ é usado. Se você definir os Fragmentos em arquivos separados (o que é o normal) coloque apenas o nome completo da classe.

E abaixo o arquivo de menu da aplicação (res/menu/activity_exemplo_sherlock.xml) ele é carregado no método onCreateOptionsMenu. Para tratar cada opção adicione o método onMenuItemSelected.
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
  <item
    android:id="@+id/menu_new"
    android:showAsAction="always|withText"
    android:icon="@android:drawable/ic_menu_add"
    android:title="Novo"/>
  <item
    android:id="@+id/menu_edit"
    android:showAsAction="ifRoom|withText"
    android:icon="@android:drawable/ic_menu_edit"
    android:title="Editar"/>
  <item
    android:id="@+id/menu_listar"
    android:showAsAction="ifRoom"
    android:icon="@android:drawable/ic_menu_my_calendar"
    android:title="Listar Itens"/>
</menu>
O primeiro item sempre aparece com ícone e texto. No segundo, o ícone sempre aparece, mas texto só aparece (na barra) se houver espaço. O último só aparecerá se houver espaço, para vê-lo, você deverá pressionar a tecla menu (ou nos dispositivo que não tenham, esse item ficará "colapsed" mas a direita).

Ao mandar executar a aplicação devemos ter uma tela similar a abaixo.

Esse post é em homenagem aos meus alunos da FA7, que me obrigaram a aprender essa biblioteca durante a aula :) Abraço a todos!

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 30 de outubro de 2012

Gesto de Swipe no Android

Olá povo,

Recentemente precisei implementar a detecção do gesto de swipe em uma aplicação iOS que estava desenvolvendo. Achei super fácil e resolvi pesquisar como fazer o mesmo no Android. Eis que achei a solução abaixo. (Não me lembro o post original, quem souber, posta um comentário pls).
public class MainActivity extends Activity 
  implements OnClickListener, OnTouchListener {

  private GestureDetector gestureDetector;
  private TextView txtResultado;
    
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
        
    txtResultado = 
      (TextView)findViewById(R.id.txtResultado);
        
    gestureDetector = 
      new GestureDetector(this, new DetectorDeGesto());
    
    // Apesar da linha abaixo não fazer nada, 
    // não funcionou sem...
    findViewById(R.id.raizLayout).
      setOnClickListener(this);
    findViewById(R.id.raizLayout).
      setOnTouchListener(this);
  }

  @Override
  public void onClick(View v) {
    Log.d("NGVL", "onclick");
  }  
    
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
  } 

  class DetectorDeGesto 
    extends SimpleOnGestureListener {
     
    private static final int DISTANCIA_MINIMA  = 120;
    private static final int DISTANCIA_MAXIMA  = 250;
    private static final int VELOCIDADE_MINIMA = 200;
     
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, 
      float velocityX, float velocityY) {
      try {
        boolean excedeuDistanciaEmY =
          Math.abs(e1.getY() - e2.getY()) 
          > DISTANCIA_MAXIMA;
               
        if (excedeuDistanciaEmY) return false;
                
        boolean velocidadeValida =
          Math.abs(velocityX) > VELOCIDADE_MINIMA;
                
        boolean swipeLeft = 
          e1.getX() - e2.getX() > DISTANCIA_MINIMA;
        boolean swipeRight =
          e2.getX() - e1.getX() > DISTANCIA_MINIMA;
                  
        // right to left swipe
        if(swipeLeft && velocidadeValida) {
          txtResultado.setText("Left Swipe");

        // left to right swipe
        }  else if (swipeRight && velocidadeValida) {
          txtResultado.setText("Right Swipe");
        }
      } catch (Exception e) {
      }
      return false;
    }
  }  
}
A classe SimpleOnGestureListener é uma classe básica do Android que consegue detectar diversos gestos simples como scroll, double tap, entre outros. Nossa classe DetectorDeGesto, herda dessa classe e sobrescreve o método onFling, que é chamado quando o usuário está deslizando o dedo sobre a tela (esse evento é armazenado em e2) e guarda a referência de quando o usuário pressionou o dedo sobre a tela inicialmente (armazenado em e1). Nesse método verificamos a direção que foi feito o swipe, bem como a velocidade que o movimento foi feito. As distâncias mínima e máxima para considerarmos o gesto válido, assim como a velocidade mínima estão definidas nas constantes da classe.
Nossa Activity implementa as interfaces View.OnClickListener e OnTouchListener. No método onTouch, delegamos o tratamento do evento de touch para o nosso detector de Gesto. Para que isso aconteça, no onCreate, criamos um GestureDetector (que é outra classe do próprio Android) passando o nosso detector de gestos. Em seguida, pegamos a referência da view que queremos tratar o evento de touch e setamos o onTouchListener e o onClickListener.
O arquivo de layout usado no projeto é mostrado abaixo:
<RelativeLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/raizLayout"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <TextView
    android:id="@+id/txtResultado"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:textSize="40dp"
    tools:context=".MainActivity" />
</RelativeLayout>
Pronto! É só rodar a aplicação e testar SWIPE pra esquerda e direita.

Qualquer dúvida, é só deixar seus comentários.

4br4ç05,
nglauber

segunda-feira, 22 de outubro de 2012

Context Menu em Listas no Android

Olá povo,

Estava dando aula e precisei mostrar para os meus alunos um exemplo de um menu de contexto. Mas o que é um menu de contexto? É basicamente é uma lista de opções que é exibida quando o usuário dá um clique longo em um componente da tela. Isso é particularmente útil em uma lista, onde você pode realizar várias operações sobre um determinado item.
É esse exemplo que mostraremos aqui. Crie um novo projeto, e em seguida crie o arquivo menu_contexto.xml na pasta menu (essa pasta não existe, você deve criá-la).
<menu 
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:id="@+id/opcao1" 
    android:title="Opção 1"/>
  <item android:id="@+id/opcao2" 
    android:title="Opção 2"/>
  <item android:id="@+id/opcao3" 
    android:title="Opção 3"/>
</menu>
Esse arquivo contém as opções de menu que aparecerão no menu de contexto. Carregaremos essas opções no método onCreateContextMenu na nossa Activity.
public class MainActivity extends ListActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
        
    String[] nomes = {
      "Fulano", "Cicrano", "Beltrano", "João",
      "Maria", "José", "Antônio", "Francisco" };
        
    ArrayAdapter<String> adapter = 
      new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1, nomes);
    setListAdapter(adapter);
    registerForContextMenu(getListView());
  }
    
  @Override
  public void onCreateContextMenu(ContextMenu menu, 
    View v, ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    getMenuInflater().inflate(
      R.menu.menu_contexto, menu);
  }
    
  @Override
  public boolean onContextItemSelected(MenuItem item){
    AdapterContextMenuInfo info = 
      (AdapterContextMenuInfo)item.getMenuInfo();
     
    String nomeSelecionado = (String)
      getListView().getItemAtPosition(info.position);
     
    switch (item.getItemId()) {
    case R.id.opcao1:
      Toast.makeText(this, "Opção 1 - "+ 
        nomeSelecionado, Toast.LENGTH_SHORT).show();
      return true;
    case R.id.opcao2:
      Toast.makeText(this, "Opção 2 - "+ 
        nomeSelecionado, Toast.LENGTH_SHORT).show();
      return true;
    case R.id.opcao3:
      Toast.makeText(this, "Opção 3 - "+ 
        nomeSelecionado, Toast.LENGTH_SHORT).show();
      return true;   
    }
    return super.onContextItemSelected(item);
  }
}
A primeira etapa para criar o menu de contexto é chamar o método registerForContextMenu, passando um objeto View. Pode ser qualquer View, mas no nosso exemplo estamos pegando a ListView (que a classe ListActivity tem internamente). No método onCreateContextMenu utilizamos a classe MenuInflater para carregar o arquivo de menu que criamos anteriormente.
Por fim, para tratar as opções de menu, utilizamos o método onContextItemSelected. Esse método começa resolvendo um problema que temos quando usamos um menu de contexto em uma lista: quando o menu de contexto é aberto, perdemos o item que estava selecionado na lista.
Felizmente, podemos obter essa informação através do método getMenuInfo, que quando chamado em um item de menu que está associada a view, que é preenchida por um Adapter (a ListView está usando um ArrayAdapter para exibir seu conteúdo), retorna uma instância da classe AdapterContextMenuInfo. Nela podemos obter a linha que foi selecionada através do atributo position.
Para saber a opção de menu que foi selecionada, usamos o método getItemId, e comparamos com os ids que foram definidos no arquivo XML de menu. Abaixo temos a nossa aplicação em execução.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 16 de outubro de 2012

Pós Graduação em Dispositivos Móveis em Fortaleza-CE

Olá povo,

A convite do meu nobre colega Robério Gomes, estarei ministrando nos próximos finais de semana, a disciplina de Google Android na Especialização em Desenvolvimento de Sistema para Dispositivos Móveis na Faculdade FA7 em Fortaleza-CE.
Ainda nesse mesmo curso, ministrarei a disciplina de iOS, onde estudaremos como desenvolver aplicações para os dispositivos da Apple (iPhone, iPad e iPod).

Espero que dê tudo certo e que os alunos aproveitem.

4br4ç05,
nglauber


terça-feira, 9 de outubro de 2012

NSPredicate no iOS

Olá povo,

Na última Campus Party Recife, assisti uma palestra do professor Fernando Castor da UFPE, e uma coisa que ele falou me chamou a atenção (veja minuto 29:58): "Java é igual ao Marlon Brando, quando chegou era bonitão, mas hoje envelheceu"(e o ator já morreu).
Estou atualmente em um projeto com iOS, e recentemente fiz um curso de Adobe Air e outro de Windows Phone. O que pude notar, é que em todos esses ambientes, as linguagens evoluíram e estão trazendo dinamismos que facilitam bastante a vida do desenvolvedor. O conceito de Binding utilizado pelo Air e pelo WP7 é um exemplo desse tipo de coisa.
Depois dessa introdução poética :) hoje quero falar de um conceito bem bacana do Objective-C, linguagem utilizada para criar aplicativos para o iOS: os predicates. Eles permitem fazer buscas em listas de objetos sem ter que iterar sobre eles. Felipe Vasconcelos me mostrou isso e achei bem bacana! :)

#define KEY_NOME  @"nome"
#define KEY_IDADE @"idade"
#define KEY_PAI   @"pai"

@interface MinhaPessoa : NSObject
@property (strong, nonatomic) NSString *nome;
@property (assign, nonatomic) int idade;
@property (strong, nonatomic) MinhaPessoa *pai;
@end

@implementation MinhaPessoa

@end

int main(int argc, char *argv[])
{
  NSMutableArray *complexList = 
    [NSMutableArray new];

  // Adicionando um NSDictionary
  [complexList addObject: @{
    KEY_NOME  : @"Antônio",
    KEY_IDADE : @10,
    KEY_PAI   : @{ KEY_NOME : @"Fulano" }
  }];

  // Outro NSDictionary
  [complexList addObject: @{
    KEY_NOME  : @"Francisco",
    KEY_IDADE : @20,
    KEY_PAI   : @{ KEY_NOME : @"Fulano"}
  }];

  // Agora um objeto Pessoa
  MinhaPessoa *p  = [MinhaPessoa new];
  p.nome     = @"João";
  p.idade    = 30;
  p.pai      = [MinhaPessoa new];
  p.pai.nome = @"Cicrano";
  [complexList addObject:p];

  // De novo um NSDicionary
  [complexList addObject: @{
    KEY_NOME  : @"Nelson",
    KEY_IDADE : @28,
    KEY_PAI   : @{ KEY_NOME : @"Beltrano"}
  }];
  // E pra finalizar, outra Pessoa
  p          = [MinhaPessoa new];
  p.nome     = @"Glauber";
  p.idade    = 28;
  p.pai      = [MinhaPessoa new];
  p.pai.nome = @"Beltrano";
  [complexList addObject:p];

  // pai.nome (nome do pai que contenha B)
  NSString *attribute = [NSString 
    stringWithFormat:@"%@.%@", KEY_PAI, KEY_NOME];
  // predicado para a busca
  NSPredicate* predicate = [NSPredicate 
    predicateWithFormat:
      @"%K contains[cd] %@", attribute, @"B"];
  NSArray *resultado = [complexList 
    filteredArrayUsingPredicate:predicate];

  for (id item in resultado) {
    NSLog(@"%@", [item description]);
  }
}
O código acima mostra bem o dinamismo no Objective-C. Primeiro, criamos um array misturando dois tipos de objetos: NSDictionary e MinhaPessoa. Essas classes têm em comum que os atributos da classe MinhaPessoa são iguais às chaves usadas no dicionário.
Uma vez alimentado o array complexList, criamos um objeto NSPredicate que fará o filtro "pai.nome contains[cd] 'B'", ou seja, todos os objetos onde o nome do pai contenha "B". O 'cd' significa que a busca será case insensitive e que serão ignorados os acentos. Através do método filteredArrayUsingPredicate obtemos um array resultado com os objetos filtrados. Por fim, fazemos um foreach para percorrer os resultados.

Saiba mais sobre predicados aqui.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 1 de outubro de 2012

Upload de arquivos no Android

Olá povo,

Esse post tava há um bom tempo como rascunho no blog e só agora consegui terminar para postar aqui. Vou mostrar como fazer uma aplicação Android que faz upload de arquivos para um servidor web.
public class UploadActivity extends Activity {

  private TextView txtArquivo;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_upload);

    txtArquivo = (TextView)
      findViewById(R.id.txtArquivo);
  }
 
  // Ao selecionar o arquivo, preenchemos o 
  // TextView com o caminho real do arquivo
  @Override
  protected void onActivityResult(int requestCode, 
    int resultCode, Intent data) {
    super.onActivityResult(requestCode, 
      resultCode, data);

    if (resultCode == RESULT_OK) {
      if (requestCode == 0) {
        Uri selectedImageUri = data.getData();
        txtArquivo.setText(getPath(selectedImageUri));
      }
    }  
  }

  // Botão que abre a galeria de mídia para 
  // selecionar um arquivo
  public void selecionarArquivo(View v){
    Intent intent = new Intent();
    intent.setType("image/*");
    intent.setAction(Intent.ACTION_GET_CONTENT);
    startActivityForResult(Intent.createChooser(
      intent,"Selecione uma imagem"), 0);
  }
 
  // Botão que envia o arquivo selecionado
  public void enviarArquivo(View v){
    try {
      new Thread(){
        public void run(){
          executeMultipartPost();
        }
      }.start();
    } catch (Exception e) {
      Log.e("NGVL", e.getMessage());
    }  
  }
 
  // Obtém o caminho real do arquivo 
  // através do ContentProvider
  public String getPath(Uri uri) {
    String[] projection = { 
      MediaStore.Images.Media.DATA };

    Cursor cursor = getContentResolver().query(
      uri, projection, null, null, null);

    int column_index = cursor.getColumnIndexOrThrow(
      MediaStore.Images.Media.DATA);

    cursor.moveToFirst();
    return cursor.getString(column_index);
  }
 
  // Método que realmente envia o arquivo
  public void executeMultipartPost() 
    throws Exception {

    String caminhoDoArquivoNoDispositivo = 
      txtArquivo.getText().toString();

    String urlDoServidor = 
      "http://url_servidor/pagina_que_trata_upload";
    String lineEnd = "\r\n";
    String twoHyphens = "--";
    String boundary = "*****"; // Delimitador

    byte[] buffer;
    int bytesRead, bytesAvailable, bufferSize;
    int maxBufferSize = 1 * 1024 * 1024; // 1MB

    try {
      // Criando conexão com o servidor
      URL url = new URL(urlDoServidor);
      HttpURLConnection connection = 
        (HttpURLConnection) url.openConnection();

      // Conexão vai ler e escrever dados
      connection.setDoInput(true);
      connection.setDoOutput(true);
      connection.setUseCaches(false);

      // Setando método POST
      connection.setRequestMethod("POST");

      // Adicionando cabeçalhos
      connection.setRequestProperty(
        "Connection", "Keep-Alive");
      connection.setRequestProperty(
        "Content-Type",
        "multipart/form-data;boundary=" + boundary);
      connection.connect();
   
      // Escrevendo payload da requisição
      DataOutputStream outputStream = 
        new DataOutputStream(
          connection.getOutputStream());
      outputStream.writeBytes(
        twoHyphens + boundary + lineEnd);
      outputStream.writeBytes(
        "Content-Disposition: form-data; "+
        "name=\"uploadedfile\";filename=\""+ 
        caminhoDoArquivoNoDispositivo + "\"" +
        lineEnd);
      outputStream.writeBytes(lineEnd);

      // Stream para ler o arquivo
      FileInputStream fileInputStream = 
        new FileInputStream(new File(
          caminhoDoArquivoNoDispositivo));
   
      // Preparando para escrever arquivo
      bytesAvailable = fileInputStream.available();
      bufferSize = Math.min(
        bytesAvailable, maxBufferSize);
      buffer = new byte[bufferSize];

      // Lendo arquivo e escrevendo na conexão
      bytesRead = fileInputStream.read(
        buffer, 0, bufferSize);

      while (bytesRead > 0) {
        outputStream.write(buffer, 0, bufferSize);
        bytesAvailable = fileInputStream.available();
        bufferSize = Math.min(
          bytesAvailable, maxBufferSize);
        bytesRead = fileInputStream.read(
          buffer, 0, bufferSize);
      }
      outputStream.writeBytes(lineEnd);
      outputStream.writeBytes(
        twoHyphens + boundary + 
        twoHyphens + lineEnd);

      // Obtendo o código e a mensagem
      // de resposta do servidor
      int serverResponseCode = 
        connection.getResponseCode();
      String serverResponseMessage = 
        connection.getResponseMessage();

      Log.d("NGVL", serverResponseCode +" = "+ 
        serverResponseMessage);
   
      fileInputStream.close();
      outputStream.flush();
      outputStream.close();
   
    } catch (Exception ex) {
      // Exception handling
    }
  }
}
O código acima está todo comentado, então vou mostrar o arquivo de layout da aplicação. O único detalhe que eu quero ressaltar, é que utilizei uma Thread apenas pra facilitar o código, mas deveríamos utilizar uma AsyncTask ou até um Service.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >

  <TextView
    android:id="@+id/txtArquivo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Arquivo a ser enviado" />

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Selecionar arquivo" 
    android:onClick="selecionarArquivo"/>

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Enviar" 
    android:onClick="enviarArquivo"/>

</LinearLayout>
A última coisa a fazer é adicionar a permissão de INTERNET no AndroidManifest.xml.
<uses-permission 
  android:name="android.permission.INTERNET"/>
A figura abaixo mostra a aplicação em execução:

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 24 de setembro de 2012

Mapas no iOS

Olá povo,

Esse post é grande, mas é bem legal. Nele, vou falar sobre a utilização de Mapas no iOS. Conheceremos as principais classes do MapKit, e além desse objetivo principal, utlizaremos um conceito interessante do Objective-C, os blocks.
Para começar crie um novo projeto no Xcode (pode ser Single View Application sem utilizar Storyboards). Adicione os frameworks MapKit e CoreLocation à sua aplicação. Em seguida, crie uma nova classe chamada Endereço e deixe-a conforme abaixo:
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface Endereco : NSObject<MKAnnotation>

@property (strong, nonatomic) NSString *titulo;
@property (strong, nonatomic) NSString *subtitulo;

- (id)initWithCoordinate:(CLLocationCoordinate2D) c;
@end
#import "Endereco.h"

@implementation Endereco

@synthesize titulo, subtitulo;

@synthesize coordinate;

- (id)initWithCoordinate:(CLLocationCoordinate2D) c {
    coordinate = c;
    NSLog(@"%f,%f", c.latitude, c.longitude);
    return self;
}

- (NSString *)subtitle {
    return subtitulo;
}

- (NSString *) title {
    return titulo;
}
@end
A classe acima está implementando o protocolo MKAnnotation que exige que exista uma propriedade coordinate e os métodos subtitle e title. Isso porque, usaremos essa classe para representar um ponto no Mapa.
Agora deixe o ViewController da sua aplicação conforme abaixo (o nome da classe obviamente pode ser alterado...).
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import "Endereco.h"

@interface NGViewController : UIViewController 
  <UISearchBarDelegate, MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet 
  UISearchBar *searchBar;
@property (weak, nonatomic) IBOutlet 
  MKMapView *mapa;
@property (weak, nonatomic) IBOutlet 
  UISegmentedControl *segMapType;
@property (strong, nonatomic) 
  Endereco *endereco;

- (IBAction)mapViewChanged:(id)sender;

@end
#import "NGViewController.h"
#import "Endereco.h"

@implementation NGViewController

@synthesize searchBar;
@synthesize mapa;
@synthesize segMapType;
@synthesize endereco;

#define API_KEY @"SUA_CHAVE"
#define TEMPLATE_URL 
  @"http://maps.google.com/maps/geo?q=%@&output=csv&key=%@"

#pragma mark - View lifecycle
- (void)viewDidUnload {
    [self setSearchBar:nil];
    [self setMapa:nil];
    [self setSegMapType:nil];
    [super viewDidUnload];
}

#pragma mark - SegmentedControl value changed
- (IBAction)mapViewChanged:(id)sender {
  // De acordo com a posição selecionada no 
  // SegmentedControl, mudamos o tipo de 
  // visualização do mapa
  switch ([segMapType selectedSegmentIndex]) {
    case 0: mapa.mapType = MKMapTypeStandard;  break;
    case 1: mapa.mapType = MKMapTypeSatellite; break;
    case 2: mapa.mapType = MKMapTypeHybrid;    break;
  }
}

#pragma mark - SearchBar methods
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
  // Ao clicar em cancelar na barra de busca, 
  // ocultamos o teclado
  [self.searchBar resignFirstResponder];
}

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
  [self.searchBar resignFirstResponder];    
    
  [self retrieveLocation:self.searchBar.text];
}

#pragma mark - Utility Methods

- (void) retrieveLocation:(NSString *)search {
  // Criando URL de requisição
  NSString *query = 
    [search stringByAddingPercentEscapesUsingEncoding:
      NSUTF8StringEncoding]; 
   
  NSString *urlString = [NSString stringWithFormat:
    TEMPLATE_URL, query, API_KEY];
    
  // Preparando request
  NSURLRequest *request = [[NSURLRequest alloc]
    initWithURL:[NSURL URLWithString:urlString]];
    
  // Declarando o Block "trataResposta" para tratar 
  // retorno. Esse block é uma função que retorna 
  // void e recebe 3 parâmetros: uma response, os 
  // dados e um erro. 
  id trataResposta = ^(NSURLResponse *response, 
    NSData *data, NSError *error) {
      if (error){
        [self showErrorDialog];
            
      } else {
        [self trataResposta:data];
      }
    };
    
  // Fazendo a requisição assícrona para evitar que 
  // a tela pareça estar travada. Aqui usamos o 
  // recurso de blocks, declarado acima e que será 
  // chamado após a requisição ser concluída
  [NSURLConnection sendAsynchronousRequest:request 
    queue:[NSOperationQueue mainQueue] 
      completionHandler:trataResposta];
}

- (void) trataResposta:(NSData *)data {
  NSString *locationString = [[NSString alloc]
    initWithData:data encoding:NSUTF8StringEncoding];

  // Nesse array, o primeiro elemento é o código de 
  // resposta HTTP (200 é sucesso) os próximos (se 
  // deu tudo certo) é a latitude e longitude.
  NSArray *listItems = [locationString 
    componentsSeparatedByString:@","];
    
  double longitude = 0;
  double latitude = 0;
    
  if ([listItems count] > 0 && 
     [[listItems objectAtIndex:0] 
       isEqualToString:@"200"]){

    latitude = [[listItems objectAtIndex:2] 
      doubleValue];
    longitude = [[listItems objectAtIndex:3] 
      doubleValue];
  }
    
  // Cria o objeto location fazer obter informações 
  // do local via GeoCoder
  CLLocation *location = [[CLLocation alloc] 
    initWithLatitude:latitude longitude:longitude];
  [self retrieveGeocoder:location];
}

- (void) retrieveGeocoder:(CLLocation *)location {
  CLGeocoder *geocoder = [[CLGeocoder alloc] init];
    
  // Mais uma chamada assícrona que recebe 
  // um block como parâmetro
  [geocoder reverseGeocodeLocation:location 
    completionHandler:
      ^(NSArray *placemarks, NSError *error) {
         
    if ([placemarks count] > 0){
             
      CLPlacemark *placemark = 
        [placemarks objectAtIndex:0];
             
      [self addAnnotation:location.coordinate 
         withMark:placemark];
             
    } else {
      [self showErrorDialog];
    }
  }];
}

- (void)showErrorDialog {
  UIAlertView *alert = [[UIAlertView alloc] 
    initWithTitle:@"Erro" 
    message:@"Erro ao obter dados da localização" 
    delegate:nil cancelButtonTitle:@"OK" 
    otherButtonTitles:nil, nil];
  [alert show];
}

- (void)addAnnotation:
  (CLLocationCoordinate2D)coordinate 
   withMark:(CLPlacemark *)placemark {

  // Se já houver uma marcação no mapa, remova
  if (self.endereco != nil) {
    [mapa removeAnnotation:self.endereco];
    self.endereco = nil;
  }
    
  // Criando MKNotation para adicionar no Mapa
  self.endereco = [[Endereco alloc]
  initWithCoordinate:coordinate];
    
  // Configura o título e subtítulo da MKNotation 
  // baseado no CLPlacemark
  self.endereco.titulo = [NSString stringWithFormat:
    @"%@, %@\n%@", 
      placemark.locality, 
      placemark.administrativeArea, 
      placemark.postalCode];
    
  self.endereco.subtitulo = 
    [NSString stringWithFormat:@"%@, %f,%f", 
      placemark.thoroughfare,
      coordinate.latitude, 
      coordinate.longitude];
    
  // Adiciona a notação ao mapa
  [self.mapa addAnnotation:self.endereco];

  /* A MKCoordinateSpan representa a quantidade 
     de área usada pela view e é usada para 
     determinar o zoom. Quanto maior o valor, 
     maior a área visível. */
  MKCoordinateSpan span;
  span.latitudeDelta = 0.02;
  span.longitudeDelta = 0.02;
    
  // A MKCoordinateRegion serve para movimentar o 
  // mapa até a região determinada no MKNotation
  MKCoordinateRegion region;
  region.center = coordinate;
  region.span = span;
    
  [self.mapa setRegion:region animated:YES];
  [self.mapa regionThatFits:region]; 
}

#pragma mark - MapView
-(MKAnnotationView *)mapView:(MKMapView *)mapView 
  viewForAnnotation:(id<MKAnnotation>)annotation {
  
  /* Ao adicionar uma notação no mapa, esse método 
     é chamado. É aqui que criamos o pin que aparece
     no mapa */
  MKPinAnnotationView *pin = 
    [[MKPinAnnotationView alloc] initWithAnnotation:
      annotation reuseIdentifier:@"currentLoc"];
    
  pin.pinColor = MKPinAnnotationColorGreen;
  pin.animatesDrop = TRUE;
  pin.canShowCallout = YES;
  pin.calloutOffset = CGPointMake(-5, 5);
    
  return pin;
}

@end
O código está comentado, qualquer dúvida, deixe seus comentários. Agora abra o XIB referente a essa tela e adicione uma SearchBar, um MapView e um SegmentedControl de modo que ele fique conforme a figura abaixo:
No SearchBar, ligue-o com a propriedade searchBar e sete o delegate. Para o MapView, faça o mesmo com a propriedade mapa. E por fim, com o SegmentedControl ligue-o com a propriedade segMapType,  e no evento Value Changed ligue com o método mapViewChanged.

Agora é só rodar e pesquisar pelos lugares desejados.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

domingo, 16 de setembro de 2012

Paginação com Swipe no Android

Olá povo,

O versão 20 do plugin ADT (Android Development Toolkit) trouxe algumas novidades, entre elas, alguns templates de projetos que podemos optar na criação de um novo projeto Android. Hoje vou  explicar cada um desses templates e ver como utilizar o template que permite fazer uma paginação similar a do Google Play mostrada abaixo.


Note que a tela "Mais rentáveis" está em evidência, mas podemos notar que há uma opção a esquerda e outra a direita na parte superior. Esse é o intuito da paginação, que permitirá ao usuário exibir a próxima informação apenas deslizando para os lados. Vamos mostrar como fazer isso na sua aplicação.
Inicie a criação de um novo projeto e preencha os campos conforme abaixo e pressione Next.


Na segunda tela, há a opção de criarmos uma Activity em branco ou do tipo Mestre/Detalhe. A primeira cria uma Activity simples,  já a segunda, cria uma Activity que exibe uma listagem e os respectivos detalhes. Se a aplicação estiver executando em um tablet, a listagem e os detalhes de um item aparecem na mesma Activity, caso seja um smartphone, a listagem ficará em uma Activity e os detalhes serão exibidos em outra.
Selecione Blank Activity e clique em Next.
A próxima tela do assistente, solicita o nome da Activity que queremos criar, o nome do seu respectivo arquivo de layout e o título da Activity. Mas a informação mais interessante para nós aqui é o campo Navigation Type, que nos dá as seguintes opções:
A opção None não cria nenhuma opção pré-definida de navegação;
Selecionando Tabs, será criada uma estrutura básica de um Activity com abas usando uma ActionBar (que requer Android 4.1);
Tabs + Swipe é igual a anterior, mas será possível alternar entre as abas com o gesto de swipe (também requer Android 4.1);
Swipe Views + Title Strip permite alternar entre views usando swipe, e o título da view corrente é mostrado em um componente PageStrip;
A última opção é a Dropdown, que configura uma ActionBar onde o usuário poderá selecionar, através de um combobox, a tela que deseja abrir. Ao selecionar o item da lista, um Fragment será carregado (também requer Android 4.1)
Vamos utilizar a opção Swipe Views + Title Strip, pois funcionará em todas as versões do Android. Selecione-a e clique em Finish.
Uma coisa curiosa, é que para utilizar os templates acima, o assistente exige que seja selecionada a versão 4.0.1 do Android na primeira tela do assistente. Mas no caso do template que selecionamos, é possível fazer algumas alterações no projeto para que o mesmo funcione em aparelhos com versões anteriores. Tudo isso, graças a API de compatibilidade do Google (que fica na pasta libs do projeto).
1) Clique com o botão direito sobre o projeto e selecione Properties. Na tela que for exibida, selecione Android do lado esquerdo e em seguida, selecione Android 2.3 (API Level 10) e clique em OK.
2) A Activity dará erro, pois estamos importando duas classes que não existem no Android 2.3: android.app.ActionBar e android.app.FragmentTransaction. Remova esses dois imports.
3) Agora vá no AndroidManifest.xml e modifique o minSdkVersion para 10 (Android 2.3).
<uses-sdk
  android:minSdkVersion="10"
  android:targetSdkVersion="15"/>
4) Dará erro agora nas pastas values-v11, values-v14 e menu. Remova as três.
5) E para finalizar, remova o método onCreateOptionsMenu da sua Activity.

Pronto! Agora é só rodar no seu dispositivo 2.3, o resultado ficará como abaixo:
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

P.S.: Post relacionado: http://nglauber.blogspot.com.br/2011/09/pagescroll.html

quinta-feira, 6 de setembro de 2012

Artigo "C2DM no Android"

Olá povo,

A revista Java Magazine, traz em sua edição de número 107, uma matéria intitulada "Mensagens da nuvem para o Android", escrita por mim e pelo meu colega Felipe Vasconcelos.

Neste artigo, falamos sobre a Cloud To Device Messaging API (C2DM), que na última Google IO mudou para GCM (Google Cloud Messaging). A C2DM permite que um dispositivo Android receba mensagens de uma aplicação servidor, e então, tomar uma ação como obter uma informação na web.

Em diversas aplicações se faz necessário verificar se existem atualizações (de algum tipo de informação) disponíveis no servidor de tempos em tempos. Com o C2DM isso pode ser mudar, uma vez que quando o servidor tiver alguma atualização, ele poderá avisar para os clientes, que então poderão baixá-las.

Espero que vocês gostem.

4br4ç05,
nglauber

terça-feira, 28 de agosto de 2012

iOS: Lendo JSON

Olá povo,

Nesse post vou mostrar como ler JSON utilizando a API nativa do iOS. Como exemplo, vamos obter os principais tópicos do Twitter conhecidos como Top Trends.
Crie um novo projeto no Xcode, e marque para utilizar Storyboards e o ARC (Automatic Reference Counting). No ViewController principal da aplicação, declare um NSMutableArray que armazenará a lista dos tópicos que serão mostrados na UITableView. Declare também um NSMutableData, que armazenará os bytes do arquivo JSON que iremos ler na conexão HTTP.
Note que nossa classe implementa o protocolo NSURLConnectionDelegate, que contém os métodos que são chamados pela conexão HTTP. No nosso caso, estamos utilizando para ler os bytes da conexão ser travar a Thread de UI.

#import <UIKit/UIKit.h>

@interface NGViewController : UITableViewController
  <NSURLConnectionDelegate> {

  NSMutableArray *trends;
  NSMutableData *data;
}
@end
O arquivo de implementação ficará como abaixo:
#import "NGViewController.h"

@implementation NGViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  trends = [NSMutableArray new];
  data = [NSMutableData new];
    
  NSURL *url = [NSURL URLWithString:
    @"https://api.twitter.com/1/trends/23424768.json"];
  NSURLRequest *request = 
    [[NSURLRequest alloc]initWithURL:url];   
  NSURLConnection *conexao = [[NSURLConnection alloc]
    initWithRequest:request delegate:self];
  [conexao start];
}

- (void)viewDidUnload {
  [super viewDidUnload];
  trends = nil;
  data = nil;
}

// Métodos de UITableViewController

- (UITableViewCell *)tableView:
  (UITableView *)tableView 
  cellForRowAtIndexPath:(NSIndexPath *)indexPath {

  UITableViewCell *cell = [tableView
    dequeueReusableCellWithIdentifier:@"Cell"];
    
  cell.textLabel.text = 
    [trends objectAtIndex:indexPath.row];
    
  return cell;
}

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

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

// NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection
  didReceiveData:(NSData *)pdata {
  [data appendData:pdata];
}


- (void)connectionDidFinishLoading:
  (NSURLConnection *)connection {   
  id jsonObject = [NSJSONSerialization 
    JSONObjectWithData:data 
    options:NSJSONReadingMutableContainers error:nil];
    
  id jsonTrends =  [
    [jsonObject objectAtIndex:0] 
      objectForKey:@"trends"];
        
  for (NSDictionary *trend in jsonTrends) {
    [trends addObject:[trend objectForKey:@"name"]];
  }
    
  [self.tableView reloadData];
}

@end
No método viewDidLoad inicializamos nossos dois atributos e logo em seguida começamos o processo de conexão com o servidor. Notem que a URL termina com 23424768.json, esse número é o woeid do Brasil, se quiser os tópicos mundiais, basta substituir por 1.json. Mas se quiser obter os tópicos de outro país, basta acessar http://developer.yahoo.com/yql/console/ e digitar o comando select * from geo.places where text="Seu Local". Mais informações sobre woeid aqui.
Em seguida criamos um request e enviamos essa solicitação através da conexão. O parâmetro delegate indica que nossa classe será notificada sobre eventos na conexão. O método viewDidUnload liberará os recursos alocados.
Não vou comentar os métodos de UITableViewController, mais detalhes aqui. Na nossa classe implementamos dois métodos do protocolo NSURLConnectionDelegate. O método connection:didReceiveData alimentará o nosso atributo data com os bytes recebidos pela conexão. Já o método connectionDidFinishLoading será chamado quando os dados terminarem de serem lidos. É nele que estamos fazendo a leitura do JSON.
A classe NSJSONSerializarion recebe um objeto NSData e retorna um objeto. Um documento JSON em Objective-C consta basicamente de NSArray ou NSDictionary. Se acessarmos a URL que definimos no primeiro método da nossa classe, vamos visualizar o arquivo JSON e se quisermos vê-lo de uma forma mais amigável, podemos utilizar o site http://jsonviewer.stack.hu/ e colar o texto do arquivo JSON lá. Então será apresentado o documento de uma forma hierárquica. Conforme a figura abaixo:
O elemento com [] é um array, enquanto que o {} é um dicionário (contento chave/valor). Sendo assim, a variável jsonObject contém um array de apenas um elemento. Dentro desse elemento temos um array chamado trends. Esse array está sendo representado pela variável jsonTrends. Em seguida, percorremos esse array (que tem vários dictionaries) e em cada dictionary obtemos o valor da chave name, que contém o nome do tópico mais comentado.
O resultado da aplicação é mostrado abaixo:
Qualquer dúvida, deixem seus comentários,

4br4ç05,
nglauber

sábado, 18 de agosto de 2012

Pipe igual?

Olá povo,

Recebi um email com uma dúvida simples, mas interessante sobre Java, então resolvi posta aqui. O "|" (chama-se pipe ou popularmente "barra-em-pé") representa o operador OR binário. Quando utilizamos || (dois pipes) em uma expressão booleana, se a primeira for verdadeira, ele nem avalia a segunda.
boolean a = true;
int x = 0;
if (a == true || ++x > 0){
   // O valor de x não mudará
}
x continuará zero, pois a segunda expressão não será avaliada.
Mas quando utilizamos o operador em uma atribuição, estamos fazendo uma operação binária de OR.
int a = 2; // Em binário 00000010
int b = 4; // Em binário 00000100

// Vai ativar os bits de "a" (que é 2) em "b"
// Logo, em binário, ficará 00000110 que é igual a 6
b |= a; 
Você pode se perguntar: "em que isso influencia?" ou ainda "onde vou usar isso?". Essa abordagem é muito usada quando vc quer que uma propriedade possa assumir uma combinação de valores. Por exemplo, uma fonte pode ser bold, itálico e/ou sublinhado.
int bold = 1;      //00001
int italic = 2;    //00010
int underline = 4; //00100
Pra deixar uma fonte, itálico e sublinhada, usaríamos:
int fonte = bold | underline; // 00101 = 5
Aí você pode perguntar: qual a diferença entre isso e o + ? É que quando você usa o OR, se o valor já estiver setado, ele não é alterado. Continuando com o exemplo acima:
// ao invés de usar
fonte += bold; 
// igual a 6 (não seria o que queremos)

// usamos
fonte |= bold; 
// como o primeiro bit já está como 1, fonte continua 5
Já para checar se uma das opções da fonte está habilitada, usamos o operador &.
// assumindo que fonte = 5 = 00101
if (fonte & bold != 0){
  // fonte é negrito
}
Fui claro? :)

4br4ç05,
nglauber

sexta-feira, 10 de agosto de 2012

Cuide da sua AsyncTask

Olá povo,

Eu comecei a escrever esse post na Campus Party Recife 2012. Palestrei sobre Android no evento, e apresentei um exemplo que faz uma busca no Twitter e exibe o resultado dessa pesquisa.
Entretanto, durante a palestra não deu para mostrar e resolver alguns problemas básicos que aplicação apresenta. São eles:
1) O Adapter que preenche a lista não foi implementado de forma eficiente;
2) Dar um feedback visual para o usuário enquanto está baixando informações;
3) Avisar ao usuário quando não houver conexão com a internet;
4) Fazer um tratamento caso tenha ocorrido algum problema durante o parse do JSON que é retornado pelo Twitter;
5) Tratar mudança de orientação para evitar que os dados sejam baixados novamente.

O objetivo desse post é mostrar como solucionar esses problemas. Mas antes de começar, vamos mostrar as duas classes básicas da aplicação: Tweet e BuscaTwitter.
public class Tweet {
  String text;
  String profile_image_url;
 
  public String getText() { 
    return text; 
  }
  public void setText(String text) { 
    this.text = text;
  }
  public String getProfile_image_url() {
    return profile_image_url;
  }
  public void setProfile_image_url(String profile_image_url) {
    this.profile_image_url = profile_image_url;
  }
}
public class BuscaTwitter {
  List<Tweet> results;

  public List<Tweet> getResults() {
    return results;
  }
  public void setResults(List<Tweet> result) {
    this.results = result;
  }
}
A primeira mapeia representará cada tweet postado pelo usuário com a hashtag #CPRecife. E a segunda representa a lista desses tweets. Criei essas classes para utilizar a biblioteca GSON que comentarei mais adiante. Vamos agora a lista das soluções.

Solução 1
A classe TweetAdapter utiliza o padrão Adapter para preencher as informações do componente visual de lista. Diferentemente da versão criada na palestra, a versão abaixo é implementada de forma eficiente.
public class TweetAdapter extends ArrayAdapter<Tweet> {

  public TweetAdapter(
    Context context, List<Tweet> objects) {
    super(context, 0, 0, objects);
  }

  public View getView(
    int position, View convertView, ViewGroup parent) {

    ViewHolder holder;

    Tweet tweet = getItem(position);

    if (convertView == null) {
      convertView = LayoutInflater.from(
        getContext()).inflate(
          R.layout.linha_tweet, null);

      holder = new ViewHolder();
      holder.txtTexto = (TextView) 
        convertView.findViewById(R.id.textView1);
      holder.imgFoto = (ImageView) 
        convertView.findViewById(R.id.imageView1);
      convertView.setTag(holder);
    } else {
      holder = (ViewHolder) convertView.getTag();
    }

    holder.txtTexto.setText(tweet.getText());
    BitmapManager.getInstance().loadBitmap(
      tweet.getProfile_image_url(),
      holder.imgFoto);

    return convertView;
  }

  static class ViewHolder {
    ImageView imgFoto;
    TextView txtTexto;
  }
} 
Não entendeu essa classe? Clique aqui.
O arquivo de layout usado pelo adapter é mostrado abaixo:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >

  <ImageView
    android:id="@+id/imageView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_launcher" />

  <TextView
    android:id="@+id/textView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Medium Text"
    android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
No exemplo acima, estou usando a classe BitmapManager que faz o download de forma assíncrona e por demanda da foto do perfil do usuário que postou o Tweet. Além disso, ela faz um cache em memória dessas imagens. Disponibilizei essa classe aqui, mas se quiser melhora-la, você pode salvar as imagens no cartão de memória do aparelho.

Solucão 2, 3, 4 e 5 :)
As demais soluções são implementadas no Fragment e na AsyncTask. A Activity do projeto é mostrada abaixo.
public class ListTweetActivity 
  extends FragmentActivity {

  @Override
  protected void onCreate(Bundle savedInstance) {
    super.onCreate(savedInstance);
    setContentView(R.layout.activity_list_tweet);
  }
}
A Activity acima, apenas exibe o arquivo de layout abaixo, que contém apenas um fragmento:
<fragment 
  xmlns:android="http://schemas.android.com/apk/res/android"
  class="ngvl.android.cprecife.ListTweetFragment"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:tag="listTweets" />
A maior parte da solução é feita no fragmento e na AsyncTask descritas na classe abaixo. Não conhece fragments? Clique aqui.
public class ListTweetFragment 
  extends ListFragment 
  implements OnClickListener {

  private TweetAsyncTask asyncTask;
  private List<Tweet> tweets; 
  private ProgressDialog dialog;
 
  @Override
  public View onCreateView(LayoutInflater inflater, 
    ViewGroup container, Bundle savedInstanceState) {

    View layout = inflater.inflate(
      R.layout.fragment_list_tweet, container); 
  
    layout.findViewById(R.id.btnRefresh)
      .setOnClickListener(this);
  
    return layout;
  }
 
  @Override
  public void onActivityCreated(
    Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    setRetainInstance(true);
  
    if (asyncTask == null){
      iniciarDownload();
    } else if 
      (asyncTask.getStatus() == Status.RUNNING){
      showDialog();
    }
  }
 
  @Override
  public void onDetach() {
    super.onDetach();
    if (dialog != null) dialog.dismiss();
  }
 
  public void onClick(View v) {
    iniciarDownload();
  }

  private void showDialog() {
    dialog = ProgressDialog.show(getActivity(),
      "Aguarde", "Carregando tweets");
  }

  private void iniciarDownload(){
    ConnectivityManager cm = (ConnectivityManager)
      getActivity().getSystemService(
        Context.CONNECTIVITY_SERVICE);
  
    int wifi = ConnectivityManager.TYPE_WIFI;
    int mobile = ConnectivityManager.TYPE_MOBILE;
  
    if (cm.getNetworkInfo(mobile).isConnected() ||
        cm.getNetworkInfo(wifi).isConnected()){

      asyncTask = new TweetAsyncTask();
      asyncTask.execute();

    } else {
      Toast.makeText(getActivity(), 
        "Sem conexão com a internet", 
          Toast.LENGTH_SHORT).show();
    }
  }
 
  private void configuraAdapter(List<Tweet> tweets){
    TweetAdapter adapter = 
      new TweetAdapter(getActivity(), tweets);
    setListAdapter(adapter);
  }
 
  class TweetAsyncTask extends 
    AsyncTask<Void, Void, BuscaTwitter> {
  
    @Override
    protected void onPreExecute() {
      super.onPreExecute();
      showDialog();
    }
  
    @Override
    protected BuscaTwitter doInBackground(
      Void... params) {

      String url = 
        "http://search.twitter.com/"+
        "search.json?q=CPRecife";
      try {
        InputStream is = new URL(url).openStream();
        Gson gson = new Gson();
        BuscaTwitter resultadoBusca = gson.fromJson(
          new InputStreamReader(is), 
          BuscaTwitter.class);
        return resultadoBusca;

      } catch (Exception e) {
        e.printStackTrace();
      }
      return null;
    }

    @Override
    protected void onPostExecute(BuscaTwitter result) {
      super.onPostExecute(result);
      if (result != null && 
          result.getResults() != null) {
        tweets = result.getResults();
        configuraAdapter(tweets);
    
      } else {
        Toast.makeText(getActivity(), 
          "Falha ao carregar tweets", 
          Toast.LENGTH_SHORT).show();
      }
      dialog.dismiss();
    }
  }
}
Solução 2
Uma solução para dar um feedback para o usuário é a ProgressDialog. Ele será exibido no método onPreExecute, e retirado da tela no método onPostExecute. Esses métodos são chamados, como o próprio nome diz, antes e depois do download das informações. Se você não conhece a AsyncTask, da uma olhada aqui.

Solução 3
Para verificar se existe conexão com a internet, estamos utilizando a classe ConnectivityManager. Essa checagem está sendo feita no método iniciarDownload. Para utilizar essa classe é necessário adicionar a permissão ACCESS_NETWORK_STATE. Mais informações, é só olhar esse post aqui.

Solução 4
Essa é a mais simples. No método que está baixando o JSON (doInBackground) temos um try/catch, para que caso ocorra algum problema, a exceção seja capturada. Nesse caso o método retornará null. Assim, se no método onPostExecute, o parâmetro result vier nulo é porque houve algum problema. E nesse caso, mostrar uma mensagem pro usuário.

Solução 5
Essa é a parte mais complexa do post. Ao girar o aparelho, (por padrão) o Android destrói e recria a Activity que está sendo exibida. Para tratar esse comportamento, temos 3 opções conforme falei nesse post aqui. As duas primeiras (fixar uma orientação e evitar que a Activity seja recriada) não são boas opções de design de software, pois elas impedem que criemos layouts diferentes para as duas orientações, e também impedem que ao mudarmos de idioma, a Activity recarregue os textos do idioma corrente.
Sendo assim, a API de Fragmentos nos dá a opção de manter seu estado mesmo que a activity seja destruída. Fazemo isso com o método setRetainInstance do Fragment. No nosso fragmento temos três atributos:
  • a AsyncTask que ferá o download das informações;
  • uma lista com os Tweets que já foram baixados, e vai servir para que não tenhamos que baixar a informação novamente quando girarmos o aparelho; 
  • e um ProgressDialog que falamos na solução 2.
No método onActivityCreate verificamos se a AsyncTask é igual a nulo, neste caso iniciamos o download das informações. Caso contrário, verificamos se a AsyncTask ainda está executando, em caso positivo, apenas exibimos o ProgressDialog.

Ao clicar no botão refresh, não podemos solicitar que a AsyncTask execute novamente, pois semelhante a classe Thread, não podemos reusá-la. Nesse caso, precisamos criar uma nova.

Para finalizar, se vocês observarem, estou utilizando a classe Gson para ler o JSON retornado pelo Twitter. Essa classe faz parte da biblioteca GSON, e faz o parse automático de um JSON para um objeto Java, desde que eles tenham a mestra estrutura. No nosso exemplo, a estrutura é representada pelas classes BuscaTwitter e Tweet. Para baixar a biblioteca do GSON é só clicar aqui. Descompacte o arquivo e coloque o JAR na pasta libs do projeto.

O arquivo de layout do fragment é mostrado abaixo:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/LinearLayout1"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#cccccc" >
    <TextView
      android:id="@+id/textView1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:layout_weight="1"
      android:text="#CPRecife"
      android:textAppearance="?android:attr/textAppearanceLarge" />
    <ImageButton
      android:id="@+id/btnRefresh"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@android:drawable/stat_notify_sync" />
  </LinearLayout>

  <ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
</LinearLayout>

Pronto! Agora você pode executar a aplicação e girar o aparelho a vontade que tudo deve funcionar bem :)

É isso pessoal. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sábado, 14 de julho de 2012

Palestra de Android na Campus Party Recife

Olá povo,

Tive o imenso orgulho de ser convidado pelo Prof. Silvio Meira para ministrar uma palestra sobre Android na Campus Party Recife. O título da apresentação será "Sua primeira e próximas aplicações Android: como fazer em uma hora?" e acontecerá na sexta, 27/07/12 às 11:15 no cenário Pitágoras.

Na ocasião, mostrarei os principais recursos utilizados para criar aplicativos para a plataforma Android. A abordagem será bem prática, mostrando as ferramentas necessárias, estrutura de um projeto,  ferramentas que facilitam a vida dos desenvolvedores e que fazem da plataforma Google líder no mercado mobile. Abrirei espaço para que os presentes possam tirar dúvidas em relação a plataforma de uma maneira geral.

Falando um pouquinho sobre a Campus Party, ela foi criada na Espanha em 1997 e é atualmente o maior evento de tecnologia, inovação, ciência, entretenimento e cultura digital do mundo. Ela funciona de forma itinerante, onde os participantes mudam-se com seus computadores, malas e barracas para dentro de uma arena, onde frequentam oficinas, palestras, conferências, competições e atividades de lazer. Em 2008, aconteceu a primeira edição brasileira, que desde então acontecia apenas em São Paulo. Entre os dias 26 e 30 de julho de 2012, Recife receberá pela primeira vez o evento. 

A #CPRecife será divida em cenários onde acontecerão palestras, oficinas, mesas de discussão, entre outras. O cenário Pitágoras será o local onde os "Campuseiros Desenvolvendo" poderão discutir/conhecer tópicos relacionados com desenvolvimento de software, software livre e segurança. Lá também estarão meus colegas Eric Cavalcanti, falando sobre programação mobile multiplataforma com Titanium, e Richardson Oliveira falando sobre desenvolvimento para a plataforma iOS.

Os ingressos para o evento já acabaram. Então, quem já comprou, nos vemos lá!

EDITADO em 27/07/2012

Vocês podem conferir como foi a palestra no vídeo abaixo:

Os slides da palestra estão disponíveis em aqui.

A aplicação feita durante a apresentação está aqui. Ela está tal como fei feita na apresentação. Aproveito pra deixar algumas sugestões de melhoria: tratar a ausência de conexão com a internet; evitar que os dados sejam recarregados ao girar o aparelho.

4br4ç05,
nglauber

quinta-feira, 12 de julho de 2012

Persistência no iOS com Core Data

Olá povo,

Eu já mostrei aqui no blog como persistir informações no iOS utilizando o SQLite. Para quem quer ter um  controle maior sobre o que está sendo salvo no banco de dados, é uma boa opção. Entretanto a Core Data realiza o mapeamento objeto-relacional de uma forma bem interessante e nos poupa de escrever um monte de código de baixo nível (uma vez que a lib do SQLite é escrita em C).

Crie um novo projeto no Xcode do tipo Single View Application e mãos à obra! Se você selecionar o template Master/Detail application no assistente, aparecerá a opção para utilizar Core Data. Isso servirá para incluir a lib do Core Data no projeto e inserir algum código para nós. Se não usar esse template, teremos que adicionar manualmente. Para isso, basta selecionar o projeto, e na aba Build Fases na seção Link Binary With Libraries e clicar em "+". Na lista que será exibida, selecione CoreData.framework.

Vamos agora adicionar ao projeto o arquivo onde ficarão as definições das entidades que serão persistidas no banco. No Xcode, selecione File | New | File... selecione a opção Core Data no lado esquerdo, e então selecione a opção Data Model. Clique em Next e nomeie o arquivo para CarroModel, em seguida,  clique em Create para concluir o assistente.

Selecione o arquivo recém criado, e clique no botão Add Entity que fica na parte inferior. Renomeie a entidade para Carro. Em seguida, clique no botão Add Attribute, selecione o atributo e o renomeie para "nome" e modifique seu tipo (na janela da direita) para String (através da propriedade Attribute Type). Adicione mais dois atributos, placa e ano, e modifique os seus tipos para String e Int16 respectivamente.
Abaixo a imagem do model após nossas alterações.
Vamos agora criar a classe carro que será persistida no banco de dados. Selecione a entidade Carro e clique no menu File | New | File... Na janela que for exibida, selecione Core Data no lado esquerdo, e então selecione NSManagedObject subclass. Será criada a classe Carro conforme abaixo.
// Carro.h -----------------------------
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>


@interface Carro : NSManagedObject

@property (nonatomic, retain) NSString * nome;
@property (nonatomic, retain) NSString * placa;
@property (nonatomic, retain) NSNumber * ano;

@end

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

@implementation Carro

@dynamic nome;
@dynamic placa;
@dynamic ano;

@end
Notem que nossa classe herda de NSManagedObject o que quer dizer que os objetos dessa classe serão persistidos. Outro detalhe é que os atributos são implementados com @dynamic.

Crie uma nova classe chamada RepositorioCarro e deixe-a conforme abaixo.

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@class Carro;

@interface RepositorioCarro : NSObject {
  NSManagedObjectModel *mom;
  NSPersistentStoreCoordinator *coordinator;
  NSManagedObjectContext *context;
}

- (void) test;
- (Carro *) newCarro;
- (void) salvar;
- (void) excluir:(Carro *)carro;
- (NSArray *) todosCarros;

@end
Nessa classe, declaramos um NSManagedObjectModel, NSPersistentStoreCoordinator e um NSManagedObjectContext. Vamos entender o papel de cada um:
- NSManagedObjectContext é a classe que vai realizar as operações com o banco. Logo, para inserir, alterar, excluir ou obter objetos (NSManagedObject) do banco, utilizamos métodos dessa classe.
- NSPersistentStoreCoordinator é como se fosse a conexão com o banco. É nela onde você define o local onde está armazenado o arquivo do banco e outras configurações adicionais.
- NSManagedObjectModel é o esquema do banco de dados. Que aqui no nosso projeto é o arquivo onde definimos o modelo da classe Carro.

O código abaixo inicia com o "construtor" da classe, que chama o método initContext. Esse método por sua vez chama o initCoordinator que por sua vez chama o initMom.

#import "RepositorioCarro.h"
#import "Carro.h"

@implementation RepositorioCarro

- (id) init {
  self = [super init];  
  [self initContext];
  return self;
}

// Retorna a URL para o diretório Documents
- (NSURL *)appDocsDir {
  return [[[NSFileManager defaultManager] 
    URLsForDirectory:NSDocumentDirectory 
      inDomains:NSUserDomainMask] lastObject];
}

// Carrega o arquivo de modelo
- (void) initMom {
  NSURL *modelURL = [[NSBundle mainBundle] 
    URLForResource:@"CarroModel" 
      withExtension:@"momd"];

  mom = [[NSManagedObjectModel alloc] 
    initWithContentsOfURL:modelURL];
}

// Abre a conexão com o banco
- (void) initCoordinator {
  [self initMom];    
  NSURL *storeURL = [[self appDocsDir] 
    URLByAppendingPathComponent:
     @"CarrosDB.sqlite"];
        
  NSError *error = nil;
  coordinator =
    [[NSPersistentStoreCoordinator alloc]
      initWithManagedObjectModel:mom];

  if (![coordinator 
    addPersistentStoreWithType:NSSQLiteStoreType
    configuration:nil URL:storeURL
    options:nil error:&error]) {
    NSLog(@"Erro... %@",
      [error localizedDescription]);
  }
}

// Inicializa o contexto
- (void) initContext {
  [self initCoordinator];
    
  context = [[NSManagedObjectContext alloc] init];
  [context setPersistentStoreCoordinator:coordinator];
}

- (void) test {
  // Testando inserção
  Carro *carro = [self newCarro];
    
  carro.nome  = @"Uno";
  carro.placa = @"UNO0001";
  carro.ano   = [NSNumber numberWithInt:2000];
    
  [self commit];

  // Testando alteração
  carro = @"Palio";
  [self commit];
  // Testando listagem...
  NSArray *fetchedObjects = [self todosCarros];
  for (Carro *carro in fetchedObjects) {
    NSLog(@"Nome: %@", carro.nome);
    NSLog(@"placa: %@", carro.placa);
  }
}

// A classe carro não deve ter construtor e 
// deve ser inicializada dessa forma.
- (Carro *) newCarro {
  return [NSEntityDescription
    insertNewObjectForEntityForName:@"Carro"
    inManagedObjectContext:context];

}

// As alterações que são feitas nos objetos
// são persistidas no banco ao dar commit
- (void) commit {
    NSError *error;
    if (![context save:&error]) {
      NSLog(@"Erro... %@", 
        [error localizedDescription]);
    }
}

- (void) excluir:(Carro *)carro {
    [context deleteObject:carro];  
}

- (NSArray *) todosCarros {
  NSFetchRequest *fetchRequest = 
    [[NSFetchRequest alloc] init];

  NSEntityDescription *entity = 
    [NSEntityDescription entityForName:@"Carro"
       inManagedObjectContext:context];

  [fetchRequest setEntity:entity];

  NSError *error;
  return [context 
    executeFetchRequest:fetchRequest 
      error:&error];
}

@end

O método newCarro cria uma nova instância de um objeto carro no contexto. Ou seja, se comitarmos esse objeto ele já estará persistido no banco. Se alterarmos uma propriedade de um objeto carro do array que é retornado pelo método todosCarros, e depois chamarmos o método commit, a alteração já é realizada no banco de dados.
O método test, como próprio nome diz, serve para testarmos nossa classe. Onde inserimos, alteramos e listamos as informações do banco.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Fonte: http://www.raywenderlich.com/934/core-data-on-ios-5-tutorial-getting-started