Quando se desenvolve, algumas situações não usuais podem ocorrer que fogem da solução arquitetada. Por exemplo, imagine-se desenvolvendo um aplicativo que contenha uma lógica de armazenamento de informações localmente num banco de dados do Android e que os recupera em um Adapter padrão, que se responsabiliza por mostrar os dados na tela por um ListView. Até então, sua aplicação está funcionando corretamente. Mas um novo requisito foi feito para ser implementado nesse aplicativo: agora precisamos que o banco de dados armazene uma imagem que será exibida no ListView. Qualquer desenvolvedor pensaria que é só adicionar um ImageView à lista que faz a chamada, certo? A ideia é boa, mas não irá funcionar.

Quando se armazena uma imagem (ou qualquer outro tipo de arquivo) no banco de dados, ele deve ter um campo específico para isso, e deve ser do tipo BLOB (Binary Large Object), tipo que é responsável por armazenar arquivos, como imagens, áudios e qualquer outro tipo de objeto multimídia. Os dados são armazenados em bytes.

Ao fazer uma chamada a um campo BLOB, o que se carregará é um Array de bytes, e é aqui se encontra o problema, pois o adaptador não irá saber como converter esse array em imagem. Portanto, o que se deve fazer é criar seu próprio adaptador que tornará capaz a conversão do array de bytes em imagem para então exibir no ListView.

Neste artigo implementaremos um adaptador customizado que carregará dados do banco, incluído um arquivo de imagem, e o mostrará em um ListView. Criaremos um aplicativo simples que armazena as informações sobre carros, como nome do carro e do dono e a foto do carro.

As operações básicas de banco de dados não serão cobertas com profundidade. Para mais informações consulte o artigo Criando um CRUD no Android com Android Studio e SQLite publicado no site.

Armazenando dados

Primeiramente será criado um banco com o campo BLOB. No Android Studio vamos criar uma classe que extende de SQLiteOpenHelper com o método onCreate, como mostra o código da Listagem 1, que será o responsável por criar a tabela.


public void onCreate(SQLiteDatabase db) {
  String sql = "create table " + TABELA
    + "(" + ID + " integer primary key autoincrement, "
    + NOME + " text, "
    + DONO + " text, "
    + FOTO + " blob )";
  db.execSQL(sql);
}
Listagem 1. Código de criação da tabela

Para armazenar uma imagem no banco de dados, primeiro devemos converte-la em bytes para então armazenar no campo BLOB do banco de dados. O procedimento é um pouco mais complexo, pois após recuperá-la de seu dispositivo ou utilizar a câmera para tirar a foto na hora, essa imagem deve ser tratada e armazenada em um objeto do tipo Bitmap e, em seguida, a imagem deve ser compactada utilizando algum padrão como JPG ou PNG (nesse artigo serão armazenadas como PNG). Após sua compactação devemos extrair seus bytes e armazená-los em um array de bytes, que será passado para a classe responsável por adicionar os dados no banco.

A Listagem 2 mostra com clareza o processo de compactação e extração dos bytes da imagem.


//Código….
Bitmap bitmap = ((BitmapDrawable)foto.getDrawable()).getBitmap();
ByteArrayOutputStream saida = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG,100,saida);
byte[] img = saida.toByteArray();
//Mais código..
Listagem 2. Compactar e extrair bytes da imagem

Observem que é muito importante o cast para BitmapDrawable, pois o getDrawable será acionado do objeto foto, que é uma instância da classe ImageView. Observem que o método compress recebe três parâmetros: o primeiro é o tipo que se quer compactar a imagem, o segundo é a qualidade dela e, o terceiro é o objeto da classe ByteArrayOutputStream, que é responsável por armazenar a imagem compactada. Por último ocorre a extração, onde tem-se um array de bytes que recebe o ByteArrayOutputStream convertido para o formato necessário.

Listar Dados

Para se listar os dados em uma lista, o que se deve fazer é criar um adaptador customizado para que o array de bytes seja convertido novamente para imagem para então ser mostrado na tela.

Na Listagem 3 temos o código XML responsável por criar o Listview.


<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" android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  tools:context="br.com.home.adaptercustomizado.ListaCarros"
  android:background="#ff000000">

  <ListView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:id="@+id/listView"
      android:layout_alignParentLeft="true"
      android:layout_alignParentStart="true"
      android:layout_alignParentTop="true" />
</RelativeLayout>
Listagem 3. XML do Listview

Na Listagem 4 tem-se o XML que organiza o conteúdo do ListView, onde serão posicionados os elementos de tela.


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <ImageView
      android:id="@+id/imageView"
      android:layout_width="50dp"
      android:layout_height="60dp"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginTop="2dp"
      android:layout_marginLeft="2dp"
      android:contentDescription="imagem" />

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textAppearance="?android:attr/textAppearanceMedium"
      android:text="Proprietário: "
      android:id="@+id/textView2"
      android:layout_alignBottom="@+id/imageView"
      android:layout_toRightOf="@+id/imageView"
      android:layout_toEndOf="@+id/imageView" />

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textAppearance="?android:attr/textAppearanceMedium"
      android:text="Medium Text"
      android:id="@+id/textView3"
      android:layout_alignTop="@+id/textView2"
      android:layout_toRightOf="@+id/textView2"
      android:layout_toEndOf="@+id/textView2" />

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textAppearance="?android:attr/textAppearanceLarge"
      android:text="Large Text"
      android:id="@+id/textView"
      android:layout_alignParentTop="true"
      android:layout_alignLeft="@+id/textView2"
      android:layout_alignStart="@+id/textView2" />

</RelativeLayout>
Listagem 4. XML responsável por estilizar ListView

A Figura 1 mostra o resultado da Listagem 4.

Demonstração da listagem 4
Figura 1. Demonstração da Listagem 4

É necessário também que se tenha uma classe Carro que armazenará as informações recuperadas do banco de dados, conforme mostra o código da Listagem 5.


package br.com.home.adaptercustomizado;
 
/**
 * Created by allanromanato on 6/2/15.
 */
public class Carro{
    private int id;
    private String nome;
    private String dono;
    private byte[] foto;
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getNome() {
        return nome;
    }
 
    public void setNome(String nome) {
        this.nome = nome;
    }
 
    public String getDono() {
        return dono;
    }
 
    public void setDono(String dono) {
        this.dono = dono;
    }
 
    public byte[] getFoto() {
        return foto;
    }
 
    public void setFoto(byte[] foto) {
        this.foto = foto;
    }
}
Listagem 5. Classe Carro

Agora o Adapter Customizado será criado para que a imagem seja exibida na tela com o ListView sem ter problemas. Observe a Listagem 6.


package br.com.home.adaptercustomizado;
 
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
 
 
import java.util.List;
 
/**
 * Created by allanromanato on 6/2/15.
 */
public class CarrosAdapter extends ArrayAdapter<Carro> {
    Context contexto;
    int id;
    List<Carro> lista;
 
    public CarrosAdapter(Context contexto, int id, List<Carro> lista){
        super(contexto,id,lista);
        this.contexto = contexto;
        this.lista = lista;
        this.id = id;
    }
 
    public View getView(int position, View convertView, ViewGroup parent){
        View view = convertView;
        Carro carro;
        ImageView foto;
        TextView nome;
        TextView dono;
        Bitmap raw;
        byte[] fotoArray;
 
        if(view == null){
            LayoutInflater inflater = LayoutInflater.from(contexto);
            view = inflater.inflate(id, parent, false);
        }
 
        nome = (TextView)view.findViewById(R.id.textView);
        dono = (TextView)view.findViewById(R.id.textView3);
        foto = (ImageView)view.findViewById(R.id.imageView);
 
        carro = lista.get(position);
 
        nome.setText(carro.getNome());
        dono.setText(carro.getDono());
        fotoArray = carro.getFoto();
 
        if(fotoArray!=null){
           raw  = BitmapFactory.decodeByteArray(fotoArray,0,fotoArray.length);
           foto.setImageBitmap(raw);
        }
 
        return view;
    }
}
Listagem 6. Adapter Customizado

Ao estender essa classe a classe ArrayAdapter obrigamos a implementar um construtor, que é responsável por receber as informações que serão utilizadas pelo adapter, como mostra o código da Listagem 7.


public CarrosAdapter(Context contexto, int id, List<Carro> lista){
    super(contexto,id,lista);
    this.contexto = contexto;
    this.lista = lista;
    this.id = id;
}
Listagem 7. Construtor

O método getView é o responsável por todas as informações que serão apresentadas no ListView, e é chamado para cada elemento iterado. Pode-se ver que a view é colocada no contexto utilizando o inflate, necessário para o correto funcionamento do adapter, pois este só é executado se uma view existe. A Listagem 8 mostra o inflate.


if(view == null){
    LayoutInflater inflater = LayoutInflater.from(contexto);
    view = inflater.inflate(id, parent, false);
}
Listagem 8. Código do Inflate

A classe LayoutInflater é responsável por instanciar um XML a sua view correspondente.

Na Listagem 9 temos o código responsável por iniciar os objetos de tela com os seus correspondentes dentro do XML de layout.


nome = (TextView)view.findViewById(R.id.textView);
dono = (TextView)view.findViewById(R.id.textView3); 
foto = (ImageView)view.findViewById(R.id.imageView);
Listagem 9. Código básico para iniciar objetos

Lembrando que quando invocamos o adapter para mostrar na tela as informações do banco, ele irá executar o código uma vez para cada registro que foi recuperado, portanto, tem-se que pegar a posição da lista que é passada por parâmetro no método getView.

A Listagem 10 demonstra o objeto Carro contido dentro da lista sendo copiado para um objeto do tipo Carro e seus conteúdos sendo colocados dentro de objetos de tela, com exceção da foto, que será explicado mais à frente.


carro = lista.get(position);

nome.setText(carro.getNome());
dono.setText(carro.getDono());
fotoArray = carro.getFoto();
Listagem 10. Código de população

Aqui vem o passo principal para exibir na tela dentro do ListView: a conversão de bytes para um Bitmap da imagem. O que se deve fazer é copiar para dentro de um objeto do tipo Bitmap o resultado da invocação do método decodeByteArray, que recebe como parâmetro o array de bytes que é a foto que se deseja converter de volta, junto o offset e o tamanho do array de bytes. Feito isso, é só fazer com que o imageView apresente a foto na tela. A Listagem 11 mostra o código responsável por essa tarefa.


if(fotoArray!=null){
   raw  = BitmapFactory.decodeByteArray(fotoArray,0,fotoArray.length);
   foto.setImageBitmap(raw);
}
Listagem 11. Código de Conversão

Para terminar esse método devemos retornar a view que sofreu o inflare no começo do método, e então o adapter está concluído.

Agora a activity principal, que chamará o adapter, deve possuir dois métodos: o método onCreate que executa automaticamente quando a activity é chamada, e o método cursor2List que recebe um cursor (resultado da consulta ao banco de dados) e retorna uma Lista de Carros, para que se possa passar ao Adapter responsável por exibir na tela as informações. A Listagem 12 mostra o código completo da activity.


package br.com.home.adaptercustomizado;
 
import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import android.widget.ListView;
 
import java.util.ArrayList;
import java.util.List;
 
/**
 * Created by allanromanato on 6/2/15.
 */
 
public class ListaCarros extends Activity {
    private CarrosAdapter adaptador;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lista_carros);
        List<Carro> lista;
        ListView controle;
        Controller dao = new Controller(getBaseContext());
        Cursor cursor = dao.carregaDados();
        lista = cursor2List(cursor);
        adaptador = new CarrosAdapter(getApplication(),
        R.layout.layout_carros, lista);
        controle = (ListView)findViewById(R.id.listView);
        controle.setAdapter(adaptador);
    }
 
    protected List<Carro> cursor2List(Cursor cursor){
        List<Carro> carros = new ArrayList<Carro>();
        cursor.moveToFirst();
        do{
            Carro carro = new Carro();
 
            carro.setNome(cursor.getString(cursor.getColumnIndex("NOME")));
            carro.setDono(cursor.getString(cursor.getColumnIndex("DONO")));
            carro.setFoto(cursor.getBlob(cursor.getColumnIndex(“FOTO")));
 
            carros.add(carro);
        }while(cursor.moveToNext());
        return carros;
    }
}
Listagem 12. Código da Activity

Na Listagem 13 temos um controller definido, que e é a classe responsável por controlar as transações com o banco de dados e, quando instanciado, o contexto tem que ser passado. Em seguida tem-se um cursor que recebe o resultado do carregaDados, método este responsável por carregar todos os dados do banco para exibi-los na tela. Por último, tem-se a conversão do cursor para lista, que será utilizada no Adapter.


Controller dao = new Controller(getBaseContext());
Cursor cursor = dao.carregaDados();
lista = cursor2List(cursor);
Listagem 13. Controller e Cursor

A Listagem 14 mostra o código da conversão de cursor para lista. Primeiramente deve-se mover o cursor para a primeira posição, garantindo assim que todos os registros serão acessados. Com isso feito, um “do .. while” é iniciado para que todos os registros que estão contidos no cursor possam ser colocados dentro da lista. Vale observar que ao acessar os setters do objeto carro, que nada mais são do que uma instância da classe Carro, precisa-se acessar, dentro do cursor, o índice que tem o nome da coluna que deseja pegar a informação (essa coluna tem o mesmo nome do campo do banco de dados). Em seguida, a string é capturada e armazenada no objeto carro através do setter que é colocado dentro da lista e uma nova iteração começa até chegar ao final do cursor. Ao acabar, a lista é retornada e está pronta para ser utilizada no adapter.


protected List<Carro> cursor2List(Cursor cursor){
  List<Carro> carros = new ArrayList<Carro>();
  cursor.moveToFirst();
  do{
      Carro carro = new Carro();

      carro.setNome(cursor.getString(cursor.getColumnIndex("NOME")));
      carro.setDono(cursor.getString(cursor.getColumnIndex(“DONO")));
      carro.setFoto(cursor.getBlob(cursor.getColumnIndex("FOTO")));

      carros.add(carro);
  }while(cursor.moveToNext());
  return carros;
}
Listagem 14. Código conversão Cursor para Lista

Para finalizar, devemos instanciar o adaptador customizado passando o layout que irá exibir cada elemento da lista e a lista de carros. O controle deve ser linkado com a view que irá exibir todos os registros da tabela e, por fim, o adaptador deve ser setado. A Listagem 15 mostra o código final.

Com tudo isso pronto, pode-se ver na Figura 2 o resultado.


adaptador = new CarrosAdapter(getApplication(),
R.layout.layout_carros, lista);
controle = (ListView)findViewById(R.id.listView);
controle.setAdapter(adaptador);
Listagem 15. Código Final
Resultado final
Figura 2. Resultado final

Pode-se notar que o ListView está apresentando todas as informações que estão armazenadas no banco de dados.

Na Listagem 16 temos as transações com o banco de dados, onde os dados são carregados e armazenados no Cursor. Veja que os dados serão definidos como somente leitura e a query é executada na tabela. O banco é fechado e o cursor é retornado para ser utilizado.


public Cursor carregaDados(){
  Cursor cursor;
  String[] campos =  {“_id","NOME","DONO","FOTO"};
  db = banco.getReadableDatabase();
  cursor = db.query("TABELA", campos, null, null, null, null, null, null);

  if(cursor!=null){
      cursor.moveToFirst();
  }
  db.close();
  return cursor;
}
Listagem 16. Consulta ao Banco de dados

Espero que tenham gostado desse artigo, o que foi mostrado aqui é algo bem simples de ser feito, tem muita utilidade no dia a dia de um desenvolvedor Android.