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

terça-feira, 26 de novembro de 2013

ContentProvider, CursorLoader e CursorAdapter

Olá povo,

Quem já assistiu minhas aulas ou alguma palestra minha sobre Android sabe que eu sempre falo que o Android tem 4 componentes básicos: Activities, Services, Broadcast Receivers e Content Providers.
Nesse post vou falar sobre ContentProvider e os benefícios de utilizá-lo junto com um CursorLoader e um CursorAdapter.

Um ContentProvider, como o próprio nome diz, é um provedor de conteúdo que nos permite compartilhar informações da nossa aplicação com outras aplicações. Essas informações normalmente são dados do nosso banco de dados SQLite (mas nada impede que venha de outra fonte). Ao meu ver, as grande vantagens de utilizar esse mecanismo são: 1) se quisermos alterar a forma de persistir os dados, teremos apenas que alterar o provider internamente; 2) podemos deixar o provider público ou privado; 3) os dados ficam sincronizados com o cursor adapter, ou seja, alterou no provider, a tela é automaticamente atualizada.

No nosso exemplo, faremos um pequeno aplicativo que salva textos no SQLite. Para tal, vamos criar uma classe que herda de SQLiteOpenHelper e que vai criar a tabela.
public class DBHelper extends SQLiteOpenHelper {

  public static final String TABLE_NAME = "messages";
  public static final String COLUMN_ID = "_id";
  public static final String COLUMN_MESSAGE="message";
 
  public static final String[] ALL_COLUMNS = { 
    COLUMN_ID, COLUMN_MESSAGE
  }; 
 
  private static final String NOME_BANCO="dbMessages";
  private static final int    VERSAO_BANCO = 1;
 
  public DBHelper(Context context) {
    super(context, NOME_BANCO, null, VERSAO_BANCO);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL(
      "CREATE TABLE "+ TABLE_NAME +" ("+ 
      COLUMN_ID+" INTEGER PRIMARY KEY AUTOINCREMENT,"+
      COLUMN_MESSAGE +" TEXT )");
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, 
    int oldVersion, int newVersion) {
    // Utilizar só na proxima versão :)
  }
}
Essa classe ficará responsável por criar o banco (e as suas tabelas) se não existir, ou atualizá-las caso já existam (em uma nova versão da app por exemplo).

Declare o Content Provider no AndroidManifest.xml.
<provider
  android:name="ngvl.android.excp.db.MessageProvider"
  android:authorities="ngvl.android.excp"
  android:exported="false" />
Agora, vamos implementar nosso Content Provider conforme abaixo.
public class MessageProvider extends ContentProvider {
  // Deve estar igual ao Manifest
  private static final String 
    AUTHORITY = "ngvl.android.excp";
  // Tipo de acesso que retorna todas as mensagens
  private static final int TYPE_ALL_MESSAGES = 1;
  // Tipo de acesso que retorna apenas uma mensagem
  // usando o id da mesma
  private static final int TYPE_SINGLE_MESSAGE = 2;

  private static final String BASE_PATH = "messages";
  // É através dessa URI que acessamos nosso provider
  public static final Uri CONTENT_URI = Uri.parse(
    "content://" + AUTHORITY + "/" + BASE_PATH);

  // Classe para checar se a Uri passada é valida 
  private static final UriMatcher sUriMatcher;

  static {
    sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    sUriMatcher.addURI(AUTHORITY, 
      BASE_PATH, TYPE_ALL_MESSAGES);
    sUriMatcher.addURI(AUTHORITY, 
      BASE_PATH + "/#", TYPE_SINGLE_MESSAGE);
  }

  private DBHelper mOpenHelper;

  @Override
  public boolean onCreate() {
    // Ao criar o Provider, inicializamos o helper
    mOpenHelper = new DBHelper(getContext());
    return true; // success
  }

  @Override
  public String getType(Uri uri) {
    return null;
  }

  @Override
  public Uri insert(Uri uri, ContentValues values) {
    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();
    long id = 0;

    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      id = sqlDB.insert(
        DBHelper.TABLE_NAME, 
        null, 
        values);
      break;
   
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);

    return Uri.parse(BASE_PATH + "/" + id);
  }

  @Override
  public int update(Uri uri, ContentValues values, 
    String selection, String[] selectionArgs) {

    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();

    int rowsUpdated = 0;

    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      rowsUpdated = sqlDB.update(
        DBHelper.TABLE_NAME, 
        values, 
        selection,
        selectionArgs);
      break;

    case TYPE_SINGLE_MESSAGE:
      String id = uri.getLastPathSegment();
      if (TextUtils.isEmpty(selection)) {
        rowsUpdated = sqlDB.update(
          DBHelper.TABLE_NAME, 
          values,
          DBHelper.COLUMN_ID + "=" + id, 
          null);
      } else {
        rowsUpdated = sqlDB.update(
          DBHelper.TABLE_NAME, 
          values,
          DBHelper.COLUMN_ID +"="+ id +
            " and "+ selection,
          selectionArgs);
      }
      break;

    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);
    return rowsUpdated;
  }

  @Override
  public int delete(Uri uri, String selection, 
    String[] selectionArgs) {

    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();

    int rowsDeleted = 0;
    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      rowsDeleted = sqlDB.delete(
        DBHelper.TABLE_NAME, 
        selection,
        selectionArgs);
      break;

    case TYPE_SINGLE_MESSAGE:
      String id = uri.getLastPathSegment();
      if (TextUtils.isEmpty(selection)) {
        rowsDeleted = sqlDB.delete(
          DBHelper.TABLE_NAME,
          DBHelper.COLUMN_ID + "=" + id, 
          null);
      } else {
        rowsDeleted = sqlDB.delete(
          DBHelper.TABLE_NAME,
          DBHelper.COLUMN_ID +"="+ id + 
            " and " + selection,
          selectionArgs);
      }
      break;
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);
    return rowsDeleted;
  }

  @Override
  public Cursor query(Uri uri, String[] projection, 
    String selection, String[] selectionArgs, 
    String sortOrder) {

    SQLiteQueryBuilder queryBuilder = 
      new SQLiteQueryBuilder();

    queryBuilder.setTables(DBHelper.TABLE_NAME);

    int uriType = sUriMatcher.match(uri);
    Cursor cursor = null;
    SQLiteDatabase db = 
      mOpenHelper.getWritableDatabase();
  
    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      cursor = queryBuilder.query(
        db, 
        projection, 
        selection,
        selectionArgs, 
        null, 
        null, 
        sortOrder);   
      break;

    case TYPE_SINGLE_MESSAGE:
      queryBuilder.appendWhere(
        DBHelper.COLUMN_ID + "= ?");

      cursor = queryBuilder.query(
        db, 
        projection, 
        selection,
        new String[]{ uri.getLastPathSegment() },
        null,
        null,
        null);
        break;
   
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    cursor.setNotificationUri(
      getContext().getContentResolver(), uri);
  
    return cursor;
  }
}
Nossa classe herda de ContentProvider e declaramos uma constante para o authority da mesma. Esse atributo deve estar igual ao que definimos no AndroidManifest.xml pois ele faz parte do caminho da Uri que utilizaremos para acessar o provider.
Nosso provedor de conteúdo responde por dois tipos de Uri: content://ngvl.android.excp/messages e content://ngvl.android.excp/messages/X onde "X" é o id da mensagem que queremos acessar.
Por isso, instanciamos um objeto do tipo UriMatcher para checar se a Uri passada bate (match) com uma das Uris disponíveis.
No onCreate instanciamos nosso SQLiteHelper, e depois temos os quatro principais métodos: insert, update, delete e query.
Os quatro métodos são bem parecidos, eles iniciam checando que tipo de Uri foi passada e instanciando o SQLiteDatabase a partir do nosso helper. Depois efetuamos a operação correspondente no banco de dados. A parte mais importante é que em todos os quatro métodos notificamos os cursores que alguma operação foi efetuada através da chamada: getContext().getContentResolver().notifyChange(uri, null).
Com isso, o adapter que estiver observando esse ContentProvider será atualizado automaticamente.

Agora vamos criar o CursorAdapter que acessará nosso provider para exibir as mensagens na tela. Ele é um pouquinho diferente do BaseAdapter, pois contém o método newView para quando uma nova View precisa ser criada e o bindView para quando precisamos apenas preencher os componentes da View.
public class MessageCursorAdapter 
  extends SimpleCursorAdapter {

  private static final int LAYOUT = 
    R.layout.item_message;

  public MessageCursorAdapter(
    Context context, Cursor cursor) {

    super(context, LAYOUT, cursor, 
      DBHelper.ALL_COLUMNS, null, 0);
  }

  @Override
  public void bindView(View view, Context context, 
    Cursor cursor) {

    TextView txtMessage = (TextView) 
      view.findViewById(R.id.txtMessage);
    TextView txtId = (TextView) 
      view.findViewById(R.id.txtId);

    txtId.setText(
      cursor.getString(
        cursor.getColumnIndex(
          DBHelper.COLUMN_ID)));
    txtMessage.setText(
      cursor.getString(
        cursor.getColumnIndex(
          DBHelper.COLUMN_MESSAGE)));
  }

  @Override
  public View newView(Context contex, Cursor cursor, 
    ViewGroup viewGroup) {

    return LayoutInflater.from(contex).inflate(
      LAYOUT, null);
  }
}
O arquivo de layout usado pelo adapter é listado abaixo.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >

  <TextView
    android:id="@+id/txtMessage"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:text="@null"
    android:textSize="20sp" />

  <TextView
    android:id="@+id/txtId"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@null"
    android:textSize="20sp"
    android:textColor="#FF0000" />
</LinearLayout>
Agora é só usar na Activity. No nosso exemplo temos um EditText, um Button e uma ListView. Ao clicar no botão, adicionamos o texto no banco de dados via ContentProvider. Ao clicar em um item da lista, o EditText é preenchido e se clicarmos em salvar o mesmo será atualizado. Se clicarmos no item, apagar o conteúdo do EditText e clicarmos em salvar, o mesmo será excluído. Assim, podemos testar todas as operações do nosso ContentProvider.
É na Activity que usamos o CursorLoader. Esse padrão é essencial no uso de banco de dados no Android, pois ele fará a consulta dos dados em segundo plano, evitando assim o bloqueio da Thread de UI, que faz com que a aplicação apareça estar travada se tivermos com uma grande quantidade de dados. A interface LoaderCallbacks notifica quando podemos criar o loader, quando a busca foi concluída e quando o cursor foi resetado por algum motivo.
public class MainActivity extends FragmentActivity 
  implements 
    LoaderManager.LoaderCallbacks<Cursor>, 
    OnClickListener, 
    OnItemClickListener {

  SimpleCursorAdapter mAdapter;
 
  EditText mEdtMessage;
 
  boolean isEditing;
  long currentMessageId;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  
    mEdtMessage = (EditText)
      findViewById(R.id.edtMessage);
    findViewById(R.id.btnAdd).setOnClickListener(this);
  
    mAdapter = new MessageCursorAdapter(this, null);
    getSupportLoaderManager().initLoader(0, null, this);
  
    ListView listView = (ListView)
      findViewById(R.id.listMessages);
    listView.setOnItemClickListener(this);
    listView.setAdapter(mAdapter);
  }

  @Override
  public Loader<Cursor> onCreateLoader(
    int id, Bundle args) {

    return new CursorLoader(
      this,
      MessageProvider.CONTENT_URI,
      DBHelper.ALL_COLUMNS,
      null, 
      null, 
      DBHelper.COLUMN_ID);
  }

  @Override
  public void onLoadFinished(
    Loader<Cursor> loader, Cursor cursor) {

    mAdapter.swapCursor(cursor);
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
  }

  @Override
  public void onClick(View v) {
    String message = mEdtMessage.getText().toString();
  
    if (!isEditing && TextUtils.isEmpty(message)){
      Toast.makeText(this, "Preencha a mensagem", 
        Toast.LENGTH_SHORT).show();
      return;
    }
  
    mEdtMessage.getText().clear();
  
    ContentValues values = new ContentValues();
    values.put(DBHelper.COLUMN_MESSAGE, message);
  
    if (isEditing){
      String whereClause = DBHelper.COLUMN_ID +" = ?";
      String[] whereArgs = new String[]{ 
        String.valueOf(currentMessageId) };

      if (TextUtils.isEmpty(message)){
        getContentResolver().delete(
          MessageProvider.CONTENT_URI, 
          whereClause,
          whereArgs);

      } else {
          getContentResolver().update(
            MessageProvider.CONTENT_URI, 
            values, 
            whereClause,
            whereArgs);
      }

    } else {
      getContentResolver().insert(
        MessageProvider.CONTENT_URI, 
        values);
    }
    isEditing = false;
  }

  @Override
  public void onItemClick(AdapterView<?> adaptView, 
    View view, int position, long id) {

    Cursor cursor = mAdapter.getCursor();
    cursor.moveToPosition(position);
  
    long messageId = cursor.getLong(
      cursor.getColumnIndex(DBHelper.COLUMN_ID));

    String messageText = cursor.getString(
      cursor.getColumnIndex(DBHelper.COLUMN_MESSAGE));
  
    currentMessageId = messageId;
    mEdtMessage.setText(messageText);
    isEditing = true;
  }
}
Pra que ninguém reclame, e peça o código fonte, segue abaixo o arquivo de layout :)
<RelativeLayout 
  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"
  tools:context=".MainActivity" >

  <Button
    android:id="@+id/btnAdd"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    android:text="Save" />

  <EditText
    android:id="@+id/edtMessage"
    android:hint="Digite a mensagem aqui"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/btnAdd"
    android:layout_alignBottom="@+id/btnAdd"
    android:layout_alignParentLeft="true"
    android:layout_toLeftOf="@+id/btnAdd"/>

  <ListView
    android:id="@+id/listMessages"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/edtMessage"
    android:layout_below="@+id/btnAdd" />

</RelativeLayout>

Bem povo, esse post ficou gigante. 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

Genymotion: o melhor emulador para Android

Olá povo,

Todo mundo que mexe com Android sabe que o emulador nativo da plataforma não é lá essas coisas. Ele demora pra abrir e é bem lento. Apesar de ser muito melhor (e recomendado) testar sua aplicação em um aparelho de verdade (aliás, em vários), o emulador pode ajudar a ver o aplicativo se comporta em outros tamanhos de tela ou versões do Android, por exemplo.
Meu aluno do TECDAM, Thomas Cristanis me deu a dica de usar o Genymotion.


Esse emulador é fantástico! Rápido, permite redimensionamento da tela, usar a webcam, e o melhor, roda o Google Play! Consequentemente todas as APIs baseadas no Play (como Google Maps v3) funcionam nesse emulador.
Para usá-lo, você terá que se registrar gratuitamente, fazer o download e seguir as etapas descritas no próprio site. Ele funciona em Windows, Mac e Linux e pode ser baixado aqui.
Ele utiliza o Virtual Box para criar uma VM com a imagem do Android. Testei com imagens de smartphones e tablets e tudo funciona muito bem.
Alguns colegas tiveram problemas com ele (no Windows, pra variar) mas isso era por conta da placa de vídeo (que não suportava GPU) ou do processador (antigo) que não suportava VMs.

4br4ç05,
nglauber

quarta-feira, 13 de novembro de 2013

Google DevBus Brasil

Olá povo,

A Google está promovendo um evento denominado Developer Bus que já passou pelas cidades de Buenos Aires, Cidade do México e Bogotá. Agora chegou a vez do Brasil receber esse evento, que acontecerá nos próximos dias 21, 22 e 23 de Novembro em São Paulo.
Mais de 2200 pessoas entre Desenvolvedores, Designers e Gerentes de Projetos se inscreveram e passaram por 3 etapas de seleção, onde apenas 40 foram selecionadas pela equipe do Google para fazerem parte desse evento, que na verdade é uma mistura de Hackaton com Reality Show. Hackaton porque os selecionados terão que desenvolver soluções para PEM (Pequenas e Médias Empresas) da América Latina utilizando as tecnologias do Google. E reality por que será transmitido ao vivo via stream diretamente do evento.
Para conseguir o objetivo em tão pouco tempo, haverão mentores do Google que são especialistas em cada tecnologia auxiliando as equipes. Como premiação, a equipe vencedora irá para Mountain View conhecer o HQ do Google.

Eu estarei lá \o/ e quando voltar vou contar como foi a experiência de ser passageiro do #DevBusBrasil.

4br4ç05,
nglauber

Mais informações:
http://codigo-google.blogspot.com.br/2013/10/o-developer-bus-chega-ao-brasil.html
http://developerbus.withgoogle.com/sao-paulo/