Mostrando postagens com marcador AsyncTask. Mostrar todas as postagens
Mostrando postagens com marcador AsyncTask. Mostrar todas as postagens

domingo, 31 de janeiro de 2016

Acessando o SQLite de forma assíncrona

Olá povo,

Não importa o quanto eu estude, sempre tem algo novo sobre Android que eu não sabia. Conversando com os meus amigos Diego Nascimento e André [Mion], descobri a classe AsyncQueryHandler que nos permite realizar as transações de banco de dados em uma Thread separada.
Acho que a maioria de vocês deve saber que quando o usuário está interagindo com o aplicativo, ele está utilizando o que chamamos de Main Thread ou UI Thread. Para que não tenhamos problemas de desempenho na renderização da UI, ela deve executar a 60 fps (frames por segundo). Se fizermos um cálculo rápido, podemos constatar que nenhuma operação que é feita na UI Thread deve demorar que 16 milissegundos. Caso contrário, perderemos frames a aplicação e o usuário começará a notar a aplicação "travando" ou demorando a responder.
Aqui no blog já falei sobre várias técnicas para não utilizar a Main Thread, tais como: AsyncTask, AsyncTaskLoader e LoaderManager. Essa última, em particular resolver parte do problema do título desse post, pois com o CursorLoader + LoaderManager podemos fazer a consulta em Content Provider em background. Perfeito! Mas... E as operações de inclusão, exclusão e atualização?
Para esses casos temos a classe AsyncQueryHandler. Essa classe facilita a realização dessas operações em background. Embora, o SQLite seja muito rápido, caso você esteja fazendo uma transação que utilize várias tabelas, ou mesmo que trabalhe com muitos registros, esse processamento poderá demorar bem mais que 16ms.
A utilização dessa classe é bem simples.
public void inserir() {
    ContentValues values = new ContentValues();
    values.put("titulo", "Lembrete");
    values.put("descricao", "Escrever post");

    Uri uri = Uri.parse("content://seu.content.provider/mensagens");

    // Começa o processo assíncrono
    new MeuAsync(getActivity()).startInsert(0, null, uri, values);
}

class MeuAsync extends  AsyncQueryHandler {
    // Usando WeakReference para evitar leak da Activity
    private WeakReference<Context> mContext;

    public MeuAsync(Context ctx) {
        super(ctx.getContentResolver());
        mContext = new WeakReference<>(ctx);
    }

    @Override
    protected void onInsertComplete(int token, Object cookie, Uri uri) {
        super.onInsertComplete(token, cookie, uri);
        if (mContext.get() != null) {
            Toast.makeText(mContext.get(),
                    "Registro inserido com sucesso!",
                    Toast.LENGTH_SHORT).show();
        }
    }
}
Perceba que estamos utilizando o método startInsert() passando como parâmetro: um identificador para essa transação; um objeto qualquer que podemos utilizar posteriormente no método de callback (aqui passamos null); a URI para o seu Content Provider; e os valores que serão inseridos.
De forma semelhando, poderíamos utilizar os métodos: startUpdate(), startDelete() e startQuery().

Na classe MeuAsync, ficam os métodos que serão chamados após a realização da operação em background. No nosso exemplo utilizamos apenas o o onInsertComplete. Mas nos casos de atualização, exclusão e busca de registros, utilizaríamos os métodos onUpdateComplete(), onDeleteComplete e onQueryComplete().

4br4ç05,
nglauber

sexta-feira, 29 de novembro de 2013

Comunicação HTTP eficiente no Android com Volley

Olá povo,

Conversando com meus novos mentores Neto Marin, Marcelo Quinta e Lúcio Maciel que conheci no DevBus Brasil, percebi uma unanimidade no que diz respeito a acesso HTTP no Android: a biblioteca Volley. Quando assisti o vídeo do Ficus Kirkpatrick no Google IO de 2013 não dei muita bola, mas os meus novos "orientadores" me mostraram o quanto essa biblioteca é poderosa e fácil de usar. Tudo isso aliado ao fato dela ser mantida pelo próprio Google.
Eu já falei em outros posts aqui do blog como fazer comunicação HTTP com AsyncTask (link 1 e link 2) e com AsyncTaskLoader (link), assim como carregar imagens da web em um Adapter usando o UniversalImageLoader (link). E também como ler um JSON de um WebService REST (link). Mas com o Volley podemos resolver todos os problemas citados nos posts anteriores e ainda remover todo aquele boiler plate que fazemos ao realizar essas requisições, simplificando o código. Além disso, ela tem as seguintes vantagens:
  • Comunicação paralela com possibilidade de priorização. Por exemplo, podemos criar uma fila de requisições JSON e outra de requisição de imagens e dizer que a primeira tem maior prioridade que a segunda;
  • Quando giramos a Activity, a mesma é destruída (mostrei como resolver isso aqui) e se nesse momento estiver havendo alguma requisição HTTP, o resultado é perdido. Podemos implementar algum tipo de cache em memória ou em banco, mas o Volley já faz esse serviço pra gente.
  • Um "ImageLoader" para carregar imagens da web, particularmente útil em adapters (falei de adapter aqui).
Depois dessa breve introdução, vamos por a mão na massa!

O Volley está em um repositório git no código-fonte do próprio Android. Então você terá que fazer um clone do mesmo via comando. Se você não tem o git instalado na sua máquina, siga esse tutorial aqui. Depois é só digitar no terminal.
git clone https://android.googlesource.com/platform/frameworks/volley

Feito isso, importe o projeto do Volley dentro do Eclipse e marque-o como biblioteca clicando com o botão direito sobre o projeto e selecionando Properties. Em seguida, selecione a opção Android do lado esquerdo e marque a checkbox "Is Library".

Em homenagem ao Ricardo Lecheta, vou fazer uma Activity que lê o JSON de carros disponível no site do seu livro de Android (que por sinal é muito bom e uso nas minhas aulas). E para ler o JSON e as imagens dos carros, vamos usar o Volley.

Vamos começar pela classe que vai manter a fila de execução de requisições do Volley bem como seu ImageLoader. O Google recomenda que ela seja um singleton, pois podemos gerenciar quantas filas de execução podemos ter e não termos que criar várias.
public class VolleySingleton {
  private static VolleySingleton mInstance = null;
  // Fila de execução
  private RequestQueue mRequestQueue;
  // Image Loader
  private ImageLoader mImageLoader;
 
  private VolleySingleton(Context context){
    mRequestQueue = Volley.newRequestQueue(context);

    mImageLoader = new ImageLoader(this.mRequestQueue, 
      new ImageLoader.ImageCache() {
        // Usando LRU (Last Recent Used) Cache
        private final LruCache<String, Bitmap> 
          mCache = new LruCache<String, Bitmap>(10);

        public void putBitmap(
          String url, Bitmap bitmap) {
          mCache.put(url, bitmap);
        }
        public Bitmap getBitmap(String url) {
          return mCache.get(url);
        }
      });
  }
 
  public static VolleySingleton getInstance(
    Context context){

    if(mInstance == null){
      mInstance = new VolleySingleton(context);
    }
    return mInstance;
  }
 
  public RequestQueue getRequestQueue(){
    return this.mRequestQueue;
  }
 
  public ImageLoader getImageLoader(){
    return this.mImageLoader;
  }
}
Vou definir um POJO simples que representa os objetos carro que iremos listar.
public  class Carro {
  String nome;
  String imageUrl;
 
  public Carro(String nome, String imageUrl) {
    this.nome = nome;
    this.imageUrl = imageUrl;
  }
}
Abaixo temos o arquivo de layout usado no adapter. Note que estamos usando NetworkImageView ao invés do ImageView tradicional.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/itemRoot"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" >

  <com.android.volley.toolbox.NetworkImageView
    android:id="@+id/img"
    android:layout_width="140dp"
    android:layout_height="70dp"
    android:src="@drawable/ic_launcher" />

  <TextView
    android:id="@+id/txtName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:textSize="20dp"
    android:text="@null"/>
</LinearLayout>
A classe Adapter listada abaixo herda de ArrayAdapter e está usando o NetworkImageView e o ImageLoader do Volley.
public class CarroAdapter extends ArrayAdapter<Carro>{

  static final int LAYOUT = R.layout.item_lista;
 
  public CarroAdapter(Context context, 
    List<Carro> objects) {

    super(context, LAYOUT, objects);
  }

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

    Context ctx = parent.getContext();
    if (convertView == null){
      convertView = LayoutInflater.from(ctx)
        .inflate(R.layout.item_lista, null);
    }
    NetworkImageView img = (NetworkImageView)
      convertView.findViewById(R.id.img);
    TextView txt = (TextView)
      convertView.findViewById(R.id.txtName);
  
    Carro carro = getItem(position);
    txt.setText(carro.nome);
    img.setImageUrl(
      carro.imageUrl, 
      VolleySingleton.getInstance(
        getContext()).getImageLoader());
  
    return convertView;
  }
}
E finalmente a Activity... Ela implementa duas interfaces: Response.Listener e Response.ErrorListener. O método da primeira (onResponse) será chamada quando a requisição ocorrer sem problemas, e da segunda (onErrorResponse) caso contrário. No onCreate, obtemos a fila de execução a partir do nosso singleton, e depois criamos um JsonObjectRequest. No Volley, além desse tipo de Request, temos o ImageRequest e o StringRequest, o primeiro para imagens e o segundo para qualquer requisição que retorne uma String. Após criar a requisição, a adicionamos na fila para ser executada.
No método onResponse já recebemos o JSONObject, então é só fazer o parse do mesmo. Aqui poderíamos usar o Gson, mas eu não gosto dele :p
Após transformar o JSONObject em uma Lista de Carros, criamos e setamos o adapter da nossa ListActivity.
Se alguma coisa der errado, o método onErrorResponse será chamado, e daí estamos exibindo um Toast.
public class MainActivity extends ListActivity 
  implements 
    Response.Listener<JSONObject>, 
    Response.ErrorListener {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String url="http://www.livroandroid.com.br/livro/"+
      "carros/carros_classicos.json";
  
    RequestQueue queue = Volley.newRequestQueue(this);

    JsonObjectRequest jsObjRequest = 
      new JsonObjectRequest(
        Request.Method.GET, // Requisição via HTTP_GET
        url,   // url da requisição
        null,  // JSONObject a ser enviado via POST
        this,  // Response.Listener
        this); // Response.ErrorListener
  
    queue.add(jsObjRequest);  
  }

  @Override
  public void onResponse(JSONObject response) {
    List<Carro> carros = new ArrayList<Carro>();
  
    try {
      // Não precisamos converter o 
      // InputStream em String \o/
      JSONObject jsonCarros = 
        response.getJSONObject("carros");
      JSONArray jsonCarro = 
        jsonCarros.getJSONArray("carro");
   
      for (int i = 0; i < jsonCarro.length(); i++) {
        JSONObject jsonCarroItem = 
          jsonCarro.getJSONObject(i);
        String nome = 
          jsonCarroItem.getString("nome");
        String thumbnail = 
          jsonCarroItem.getString("url_foto");
    
        Carro carro = new Carro(nome, thumbnail);
        carros.add(carro);
      }
    } catch (Exception e){
      e.printStackTrace();
    }
   
    setListAdapter(new CarroAdapter(this, carros));
  }
 
  @Override
  public void onErrorResponse(VolleyError error) {
    Toast.makeText(this, "Erro!", 
      Toast.LENGTH_SHORT).show();
  }
}
Ah! Como toda app Android que acessa a Web, adicione a permissão de Internet no seu AndroidManifest.xml.

É isso, a partir de agora podemos utilizar o Volley em nossas aplicações com essa biblioteca usada e mantida pelo próprio Google. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

domingo, 17 de novembro de 2013

Universal Image Loader

Olá povo,

Quem nunca precisou carregar imagens vindas da rede ou do cartão de memória em um Adapter no Android? Nativamente não temos uma maneira simples de fazer isso (porque hein Google?). Se assim como eu, você sempre buscava alguma solução alternativa, eu recomendo o Universal Image Loader.

Ele é o mais simples de usar que achei, e o que fornece mais recursos, pois nos permite setar uma imagem para uma ImageView passando apenas uma URI, que pode ser:
// da Web
String imageUri = "http://site.com/image.png"; 
// do SD card
String imageUri = "file:///mnt/sdcard/image.png"; 
// de um content provider
String imageUri = 
  "content://media/external/audio/albumart/13"; 
// da pasta assets do seu projeto
String imageUri = "assets://image.png"; 
// da pasta res/drawable
String imageUri = "drawable://" + R.drawable.image; 
Foi essa biblioteca que utilizei no aplicativo do blog. E como pode ser visto no site deles, já foi utilizada e aprovada por dezenas de aplicações.
Os passos da utilização são bem simples e estão descritos no site, mas vou colocar aqui a maneira mais simples. 1) Faça o download do jar e adicione na pasta libs do seu projeto. 2) Adicione as permissões de Internet e de escrita no cartão de memória no AndroidManifest.xml. Além disso, vamos criar uma classe que herda de Application e também declará-la no manifest. Essa classe é o melhor lugar para inicializarmos os singletons da nossa aplicação, pois ela é ponto de partida da mesma.

<uses-permission 
  android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission 
  android:name="android.permission.INTERNET" />

<application
  android:name="pacote.da.app.ClasseDaSuaApp" ... >
Agora adicione a classe da sua app que inicializará o Universal Image Loader.
public class ClasseDaSuaApp 
  extends Application {

  @Override
  public void onCreate() {
    super.onCreate();
    DisplayImageOptions defaultOptions = 
      new DisplayImageOptions.Builder()
        .cacheInMemory(true)
        .cacheOnDisc(true)
        .build();

    ImageLoaderConfiguration config = 
      new ImageLoaderConfiguration.Builder(
          getApplicationContext())
        .defaultDisplayImageOptions(defaultOptions)
        .build();

    ImageLoader.getInstance().init(config);
  }
}
No código acima, ativamos a opção de cache em memória e em disco. Feito isso, é só utilizar no seu adapter lá no getView.
String imgUrl = entry.thumbnailURL;
holder.imgThumbnail.setImageResource(R.drawable.ic_launcher);
if (!TextUtils.isEmpty(imgUrl)){
  ImageLoader.getInstance().displayImage(imgUrl, holder.imgThumbnail);
}
Qualquer dúvida, consultem o site da biblioteca no github.

4br4ç05,
nglauber

quarta-feira, 1 de maio de 2013

AsyncTaskLoader

Olá povo,

Uma das coisas mais comuns em aplicações móveis é o acesso a servidores web através do protocolo HTTP. O Android obviamente nos permite esse tipo de comunicação de várias formas. Mas uma forma interessante foi introduzida na versão 3 da plataforma: os Loaders. Esse tipo de abordagem permite isolar a Activity da classe que está fazendo o download dos dados.
Um exemplo típico dessa abordagem é a utilização da classe AsyncTask - que eu sempre usei, e ainda uso - jutamente com um ProgressDialog. Normalmente o ProgressDialog era um atributo da AsyncTask  e no onPreExecute ele era exibido, sendo ocultado no onPostExecute. O problema dessa abordagem é que ao rotacionar a tela do aparelho, a Activity é recriada. Dessa forma, quando a AsyncTask terminava seu trabalho, a instância do Dialog armazenada ficava apontando para a instância da Activity que o tinha criado, gerando uma exceção em tempo de execução.

Com a chegada dos Fragments, é possível reter a instância do mesmo e ter um controle melhor sobre a rotação da tela (e outras mudanças de configuração). Mas a abordagem que eu achei interessante é a utilização da classe AsyncTaskLoader em conjunto com o LoadManager e a interface LoaderCallbacks. Vejamos o exemplo abaixo:
public class MainActivity 
  extends FragmentActivity 
  implements LoaderCallbacks<String> {

  ProgressDialog dialog;
  String json;
 
  private static final int MEU_LOADER = 0;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Recuperando o estado da Activity
    if (savedInstanceState != null) {
      json = savedInstanceState.getString("valores");
      atualizaTela();
    }

    // Essa classe gerencia todos os Loaders
    LoaderManager lm = getSupportLoaderManager();

    // Se o JSON ainda não foi baixado
    if (json == null) {
      // Exibe o dialog
      dialog = ProgressDialog.show(
        this, "Aguarde...", "Baixando tweets");

      // E inicializa o Loader (se ele já foi 
      // inicializado, ele apenas continuará)
      lm.initLoader(MEU_LOADER, null, this);
    }
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    // Se estiver exibindo o progress, remova-o 
    if (dialog != null && dialog.isShowing()) 
      dialog.dismiss();
  } 

  @Override
  protected void onSaveInstanceState(Bundle outState){
    super.onSaveInstanceState(outState);
    // Salvando o estado da Activity
    outState.putString("valores", json);
  }

  // Métodos da interface LoaderCallbacks -----------
  @Override
  public Loader<String> onCreateLoader(
    int id, Bundle args) {
    // Instancia e retorna a AsyncTaskLoader
    return new MinhaAsyncTask(this);
  }

  @Override
  public void onLoadFinished(Loader<String> loader, 
    String dados) {
    // Chamado quando o Loader termina seu trabalho
    // Aqui, atribuímos o resultado ao 
    // atributo json e ocultamos o dialog
    json = dados;
    atualizaTela();
    dialog.dismiss();
  }

  @Override
  public void onLoaderReset(Loader<String> loader) {
     // Chamado se o Loader for resetado
     Log.d("NGVL", "onLoaderReset");
  }

  // AsyncTaskLoader
  public static class MinhaAsyncTask 
    extends AsyncTaskLoader<String> {

    // O JSON baixado será armazenado aqui para ser 
    // repassado para Activity
    String data;

    public MinhaAsyncTask(Context context) {
      super(context);
    }

    @Override
    protected void onStartLoading() {
      // Se não baixou os dados, faça agora!
      if (data == null) {
        forceLoad();

      // Se já baixou, apenas entregue o resultado 
      } else {
        deliverResult(data);
      }
    }

    @Override
    public String loadInBackground() {
      // Nesse método será feito o download do JSON
      return downloadJSON();
    }
  }
}
O código acima está todo comentado. Então vou focar nos detalhes que devem ser observados.
O primeiro ponto é utilizar as classes do pacote android.support.v4.* pois o conceito de Loaders só surgiu no Honeycomb, então utilizei aqui a API de compatibilidade para que esse código execute sem problemas nas versões anteriores ao Android 3. O segundo ponto interessante é que, com essa abordagem o download não é reiniciado quando a Activity é recriada.
Outro detalhe é que, caso você queira chamar o loader novamente, você deve usar o método restartLoader e não o initLoader. E se quiser passar parâmetros para o Loader, pode usar um objeto Bundle e passá-lo como o segundo argumento desses métodos.
getSupportLoaderManager().restartLoader(
  MEU_LOADER, null, this);
Os métodos downloadJSON() e atualizaTela() devem ser implementados por você :) para baixar o JSON desejado e atualizar a tela da maneira apropriada.
Abaixo eu coloquei um exemplo desses dois métodos que baixa o JSON de uma busca no Twitter e exibe os textos dos Tweets em uma ListView.
private void atualizaLista() {
  if (json == null) return;
  try {
    JSONObject jsonObj = new JSONObject(json);
    JSONArray jsonResults = 
      jsonObj.getJSONArray("results");
   
    List<String> texts = new ArrayList<String>();
    for (int i = 0; i < jsonResults.length(); i++) {
      JSONObject result = jsonResults.getJSONObject(i);
      texts.add(result.getString("text"));
    }
   
    ListView list = (ListView)
      findViewById(R.id.ListView1);

    list.setAdapter(new ArrayAdapter<String>(this, 
      android.R.layout.simple_list_item_1, texts));
   
  } catch (JSONException e) {
    e.printStackTrace();
  }
}
 
private static String downloadJSON() {
  try {
    URL url = new URL(
      "http://search.twitter.com/search.json?q=jctransito");
    HttpURLConnection conexao = (HttpURLConnection)
      url.openConnection();
    conexao.connect();
   
    InputStream is = conexao.getInputStream();
    BufferedReader reader = new BufferedReader(
      new InputStreamReader(is));

    String s = reader.readLine();
    return s;
   
  } catch (Exception e) {
    e.printStackTrace();
  }
  return null;
}
Não esqueça de adicionar a permissão de INTERNET no AndroidManifest.xml.

Com abordagem acima, mesmo ao girar o aparelho, o download continuará, e caso não tenha terminado quando a Activity carregar, o ProgressDialog será exibido novamente.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Referências:
http://developer.android.com/reference/android/app/LoaderManager.html
http://developer.android.com/reference/android/app/LoaderManager.LoaderCallbacks.html
http://developer.android.com/reference/android/content/AsyncTaskLoader.html

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

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

sexta-feira, 17 de junho de 2011

Tarefas assíncronas no Android

Olá pessoal,

Em muitos momentos será necessário a comunicação com um servidor web para fazer a integração da aplicação mobile. Essa atividade deve ser feita em uma Thread separada da UI, entretanto precisamos dar um feedback para o usuário de que essa operação está sendo executada. Para esse tipo de tarefa o Android disponibiliza a classe AsyncTask. Ele permite configurar o que será feito antes, durante e depois da execução de uma tarefa.
No código eu vou acessar a página do dicionário Michaelis para obter a tradução de uma determinada palavra. No onClick do botão, estou iniciando uma nova AsyncTask. Notem que foi passado o conteúdo da caixa de texto como parâmetro pro método execute.
O método downloadArquivo baixará o conteúdo HTML que é gerado pelo site do Michaelis.

public class PrincipalActivity extends Activity
implements OnClickListener {

private String URL_TXT  =
  "http://michaelis.uol.com.br/moderno/portugues/"+
  "index.php?lingua=portugues-portugues&"+
  "typeToSearchRadio=exactly&pagRadio=50&palavra=";
private TextView txtHTML;
private EditText edtTexto;
 
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.teste_async);

  findViewById(R.id.button1).setOnClickListener(this);

  txtHTML = (TextView)findViewById(R.id.textView2);
  edtTexto = (EditText)findViewById(R.id.editText1);
}

public void onClick(View v) {
  new MinhaAsyncTask().execute(
    edtTexto.getText().toString());
}

public String downloadArquivo(String url) {
  HttpURLConnection connection = null;
  InputStream is = null;
  ByteArrayOutputStream bos = null;
  String s = null;
  
  try {
    URL urlAddr = new URL(url);
    connection = (HttpURLConnection)
      urlAddr.openConnection();

    connection.setRequestMethod("GET");
    connection.setDoInput(true);     
    connection.setDoOutput(false);
    connection.connect();
   
    is = connection.getInputStream();
   
    bos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int length;
   
    while ((length = is.read(buffer)) > 0 ) {
      bos.write(buffer, 0, length);
    }
    
    s = new String(bos.toByteArray(), "ISO-8859-1");
   
  } catch (Exception e) {
    e.printStackTrace();
  }  finally {
    try {
      bos.close();
      is.close();
      connection.disconnect();
    } catch (Exception e){}
  }

  return s;
}
 
class MinhaAsyncTask 
  extends AsyncTask <String, Void, String> {

  private ProgressDialog progressDialog;

  protected void onPreExecute() {
    progressDialog = new ProgressDialog(
      PrincipalActivity.this);

    progressDialog.setMessage("Aguarde...");
    progressDialog.show();
  }

  protected String doInBackground(String... v) {
    String s = downloadArquivo(
      URL_TXT + v[0].toString());

    return s;
  }

  protected void onPostExecute(String result) {
    txtHTML.setText(result);
    progressDialog.dismiss();
  }
}
}


Nossa AsyncTask é declarado recebendo 3 valores genéricos. O primeiro é o tipo do dado que será passado como parâmetro na para o método que executará em segundo plano (doInBackground). O segundo é o tipo do dado que pode ser usado para enviar o progresso de alguma execução da thread background (onProgressUpdate). E o último é o tipo de retorno da ação que será realizada em segundo plano.
No nosso caso usamos: uma String, que será a palavra a ser buscada; Void, já que não usarei o progresso com números; e a String que será o HTML lido.

EDITADO 19/06/2012
Para o Android, toda aplicação que possa onerar ou por em risco os dados do usuário deve requerer uma permissão para pode utilizar determinado recurso. No caso da nossa aplicação é a permissão de acesso a internet. Adicione a permissão abaixo dentro da tag do AndroidManifest.xml
<uses-permission
  android:name="android.permission.INTERNET" />

Esse post foi baseado no post sobre AsyncTask do blog Eu, Android e foi enviado pelo meu aluno Bruno Baudel. Qualquer dúvida, olhem o blog dos caras e deixem seus comentários.

4br4ç05,
nglauber