segunda-feira, 11 de novembro de 2013

Adapters


Olá à todos.


Neste post falaremos de Adapters.


Um Adapter funciona como uma ponte entre uma AdapterView e os dados destinados para essa View. O Adapter permite o acesso aos itens de dados. O Adapter também é responsável por fazer uma exibição para cada item no conjunto de dados. Ou seja, um Adapter representa a ligação entre a View e alguma fonte de dados. Ele geralmente vêm em dois tipos: aqueles que representam dados baseadas em array’s ou listas; aqueles que representam os dados baseados em cursor.

Neste post falaremos sobre criação de tipos relacionados à um Adapter (List, Spinner, GridView, Gallery, e também mais um pouco mais sobre banco de dados (criando um exemplo que faz a interação entre um banco de dados e um Adapter). Primeiramente, veremos os tipos de listas mais comuns em Android (ArrayAdapter, SimpleAdapter, CursorAdapter, dentre outros), para então aplicá-las em componentes de interface que associam listas com View (tipos Spinner, GridView, Gallery).

Um Adapter é uma classe de Android que intermedia a criação de listas, e também lida com outros tipos de componentes, como os tipos Spinner, GridView, Gallery e AutoCompleteTextView.

Podemos utilizar um Adapter em uma Activity de duas maneiras:

  • Extendendo de Activity. Neste caso, precisamos declarar um atributo de classe do tipo ListView, e também definir o Adapter da seguinte forma: mListView.setAdapter().
  • Extendendo de ListActivity. Neste caso, a ListView já está implícita na Activity, e definimos o Adapter através do método setListAdapter(). E não precisamos definir um layout para a Activity (como comumente é feito através do método setContentView()), uma vez que uma classe que extende de ListActivity já implementa nativamente uma ListView.


E também podemos utilizar um Adapter tanto com seu construtor (como mostraremos nas seções 1 e 2), como criando uma classe que extenda ou de ArrayAdapter ou BaseAdapter.

  • Se extendermos de ArrayAdapter, precisamos apenas sobrescrever o método getView().
  • Se extendermos de BaseAdapter, precisamos sobrescrever não somente getView(), como também getCount(), getItem() e getItemId(), que são métodos obrigatórios, e que dão referencia ao BaseAdapter.


Todas estas maneiras de se criar um Adapter serão demonstradas neste capítulo.

Este tópico será dividido em diversas seções, cada uma explicando como inflar em um layout os diferentes tipos de componentes de interface propostos. 

Seção 1 - ArrayAdapter


Este é o tipo de Adapter de lista mais comum. Usaremos um exemplo extremamente simples para demonstrar seu funcionamento. Neste exemplo, mostraremos uma lista de dispositivos mobile, com seu respectivo logo do lado esquerdo.

Esse exemplo possui apenas 2 classes, separadas nos pacotes adapter e activity. São elas: ListMobileActivity.java e MobileArrayAdapter.java.


Eis o conteúdo da classe ListMobileActivity.java:



Esta classe possui uma constante com os dados que serão usados para popular o Adapter, e também o método setListAdapter() dentro do evento onCreate(). Como esta Activity extende de ListActivity, o método setListAdapter() pode ser chamado de forma direta. Caso não fosse, teríamos que obrigatoriamente inicializar alguma classe de algum Adapter. Esta metodologia será mostrada mais adiante.

No exemplo acima, usamos o Adapter da classe . Mas também poderíamos utilizar um Adapter criado em tempo real, tal qual é mostrado pelo comando comentado na 3ª linha do evento onCreate(). Nela temos setListAdapter(new ArrayAdapter<String>(this, R.layout.list_mobile, R.id.label, MOBILE_OS));. Eis os argumentos que um constructor de ArrayAdapter possui:


  • Sua definição: public ArrayAdapter (Context context, int resource, int textViewResourceId, T[] objects);
  • Seus argumentos:
    • context O contexto atual.
    • resource O id para o arquivo de layout que contém o layout a ser usado quando instanciarmos uma View.
    • textViewResourceId O id parar a TextView do layout à ser populado.
    • objects Os objetos à serem representados na ListView.


Como podemos ver acima, um ArrayAdapter é definido para um TextView. Mas como em nosso exemplo buscamos mostrar na lista não apenas um nome, mas também uma imagem, não poderemos usar um ArrayAdapter para inflar o layout da nossa lista.

Logo, vamos à definição do MobileArrayAdapter é mostrada abaixo.



Como atributos temos o contexto à ser usado pelo Adapter, e um array de Strings com os dados à serem populados na lista. Temos também o construtor da classe, e o método getView(). É este método que popula cada item da lista, um a um. Ele possui os seguintes argumentos:


  • Definição: View getView(int position, View convertView, ViewGroup parent);
  • Argumentos:
    • int position A posição do item em meio aos dados do Adapter que queremos.
    • View convertView A antiga View para ser re-usada, se possível.
    • ViewGroup parent O pai da View atual, que esta eventualmente poderá se associar.


Neste método, primeiramente inflamos o layout que cada item da lista terá. Depois, mapeamos o texto (TextView) e a imagem (ImageView) que temos no layout, ou seja, no arquivo de XML que representa cada item da nossa lista. No caso deste exemplo, eis o layout ao qual estamos mapeando:



Seguindo o método getView(), temos a atribuição de texto e imagens aos nossos componentes de interface gráfica, através dos métodos setText() e setImageResource() (este último sendo executado após sucessivos comandos condicionais).

E é assim basicamente que inflamos um dado de uma lista em Android: inflando um layout, mapeando seus componentes de interface, e inicializando cada um deles.


Seção 1.1 - Evento de clique


Também podemos setar um evento de clique para cada elemento da nossa lista. Isto pode ser feito através do método:



Através de seus argumentos conseguimos saber qual elemento da lista foi clicado, conforme mostrado acima.

Este evento é extremamente útil para listas em geral, uma vez que cada dado de uma lista difere dos outros dados da lista, e poder acessar cada dado é algo fundamental.


Seção 1.2 - ConvertView


Suponha que tenhamos 80 elementos em uma lista. Então, o método getView() será acessado 80x, e, conforme a figura acima, o layout de cada elemento será inflado 80x. Isso acaba por se tornar muito custoso tendo em vista memória e processamento.

Uma maneira de amenizar este esforço é através do reaproveitamento do argumento convertView do método getView(). Isso é mostrado abaixo:




Ou seja, à cada acesso feito ao método getView(), verificamos se o layout já foi inflado. Se já, o reaproveitamos buscando o argumento convertView do nosso método.

Desta forma, tornamos o acesso à nossa lista muito mais eficiente.


Seção 1.3 - ViewHolder


Ainda podemos deixar nossa lista ainda mais eficiente.

O padrão ViewHolder permite evitar o método findViewById() quando reusamos uma convertView. Uma classe ViewHolder é uma classe interna e estática dentro do Adapter que armazena referências às View’s (componentes gráficos) relevantes do seu layout. Esta referência é associada à View que representa o dado corrente do layout através de uma tag e do seu método setTag().

Se recebermos um objeto convertView, podemos pegar a instância do ViewHolder via o método getTag() e associar os novos atributos às View’s via a referência do ViewHolder.

Esta metodologia de declarar o ViewHolder parece complexa. Porém, ela tem um grande benefício: ser 15% mais rápida que o método findViewById().

Eis a implementação de ViewHolder:


E como ele é chamado no método getView():




Seção 1.4 - Menu de contexto em uma lista


Além do evento de clique, podemos ter também o evento de longo-clique, que pode ser acionado através de um menu de contexto. Como fazemos isso em uma lista? Primeiramente, chamando o método registerForContextMenu().



Posteriormente, implementando o método onCreateContextMenu().



Desta forma, ao “segurarmos” em um elemento qualquer da lista, o menu de contexto será aberto, mostrando 10 opções de clique (“Menu item number 0” até “Menu item number 9”).

E se clicarmos em uma dessas opções? Também podemos controlar esta ação, através do método onContextItemSelected():


Seção 2 - SimpleAdapter


O SimpleAdapter é um tipo de Adapter tão simples como o ArrayAdapter. Em nosso exemplo, ao invés de implementarmos a lista em uma ListActivity (como no exemplo anterior), implementaremos em uma Activity.

O construtor de um SimpleAdapter é o seguinte:


  • Construtor SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to);
  • Argumentos:
    • context O contexto onde a View associada com este SimpleAdapter está rodando.
    • data Uma lista de Map. Cada entrada na lista corresponde à uma linha da lista. O Map contém o dado de cada linha, e deve incluir todas as entradas especificadas em from.
    • resource O id do layout da View que define o layout para os itens da lista.
    • from Uma lista de nomes de coluna do Map que serão adicionados à cada item associado do Map.
    • to As View que devem mostrar as colunas no parâmetro from. Todas devem ser TextView.



Assim como em um ArrayAdapter, o uso do construtor do SimpleAdapter limita bastante as opções de personalização de uma lista, tanto em relação aos seus dados, quanto ao layout dos mesmos. Por exemplo, na seção 1, foi criado um arquivo de layout específico para o ArrayAdapter, e também o Adapter da lista em uma classe particular. Mas caso precisamos mostrar apenas uma lista de texto, os construtores do ArrayAdapter e SimpleAdapter servem bem ao nosso propósito.

Eis a implementação do nosso exemplo de SimpleAdapter:


Seção 3 - CursorAdapter


O CursorAdapter é um tipo de Adapter mais específico. Com este Adapter, criamos uma lista diretamente com os dados de um Cursor. Ele é bastante utilizado em operações de banco de dados.

O exemplo que utilizaremos aqui é mais sucinto, e utiliza dados da agenda telefônica, que podem ser retornados em forma de lista de Cursor.

Mas antes do exemplo ser mostrado, vamos dar uma olhada no método construtor da classe SimpleCursorAdapter:


  • Construtor public SimpleCursorAdapter (Context context, int layout, Cursor c, String[] from, int[] to, int flags);
  • Parâmetros:
    • context O contexto onde a ListView associada com este construtor está rodando.
    • layout Id do arquivo de layout que define as View para este item de lista. O arquivo de layout deve incluir pelo menos as View definidas em "to".
    • c O cursor de banco de dados. Pode ser nula se o cursor não estiver disponível ainda.
    • from Uma lista de nomes de coluna representando os dados à serem anexados à interface. Pode ser nula se o cursor não estiver disponível ainda.
    • to As View que devem mostrar colunas no parâmetro "from". Estas devem ser todas TextView. Pode ser nula se o cursor não estiver disponível ainda.
    • flags Flags usadas para determinar o comportamento do Adapter, conforme CursorAdapter(Context, Cursor, int).

Vamos agora ao nosso exemplo:



Através do método getContacts(), obtemos todos os contatos armazenados no celular. Ele retorna um Cursor. Após isto, basta popularmos o construtor da classe SimpleCursorAdapter, e o colocarmos na lista. É tudo muito simples.

A única coisa visualmente diferente é o método startManagingCursor(). Sua função é associar o ciclo de vida do Cursor com o ciclo de vida da Activity. Ou seja, quando a Activity é paralisada, automaticamente é chamado o método deactivate() no Cursor, e quando ela é reiniciada posteriormente, é chamado o método requery() para o Cursor ser reinicializado. Quando a Activity é destruída, todos os cursores serão fechados automaticamente.

Seção 4 - Spinner


Spinners permitem um jeito rápido de selecionar um valor de um conjunto. Em seu estado padrão, um Spinner mostra seu valor corrente selecionado. Quando um Spinner é clicado, é mostrado um menu dropdown com todos os outros valores disponíveis, ao qual o usuário pode selecionar um novo.

Abaixo temos um exemplo de aplicativo implementando um Spinner:



Podemos notar que os itens do Spinner estão armazenados na constante ITEMS, e também que temos um TextView cujo texto é o elemento atual do Spinner (o que pode ser verificado pelo método onItemSelected()).

Repare também que é implementado um ArrayAdapter, e que este é inserido no Spinner. Desta forma, teremos uma lista armazenada no Spinner. Visualmente, ele se parece com um TextView. Mas assim que ocorre um evento de clique nele, é mostrada uma lista de elementos, que, quando clicados, atualizam o TextView descrito no parágrafo acima, além de atualizar o valor do próprio Spinner.

Por fim, abaixo podemos ver o layout do arquivo main.xml:



E não há mais muito segredo! A implementação de um Spinner é bem simples.

Seção 5 - GridView


GridView é um ViewGroup que mostra itens em um grid rolável e bi-dimensional. Os itens deste grid são automaticamente inseridos ao layout usando um ListAdapter. Usaremos como exemplo de GridView o mesmo exemplo apresentado na seção 1.

Para criarmos nosso exemplo, precisamos ter basicamente 4 elementos: a tela principal, o Adapter do GridView, o layout da tela principal (que mostrará um GridView), e o layout de cada elemento da lista referente à GridView.

A tela principal é bem simples:



Nesta tela simplesmente criamos uma GridView, setamos seu Adapter e seus dados, e implementamos seu evento de clique.

Seu Adapter é bem parecido com os Adapter mostrados neste post:



No layout da tela principal, basicamente possui uma GridView:



E cada elemento da lista possui um layout bem parecido ao mostrado na seção 1:


Seção 6 - Gallery


Uma Gallery é uma View comumente usada para mostrar itens em uma lista horizontalmente rolável que mostra a seleção corrente no centro.

Usaremos um exemplo bem simples pra sinalizar seu funcionamento. Nele temos uma tela, um Adapter e um layout para a tela.

A tela é bem simples, nada muito diferente do que já vimos.



A mesma coisa para o AdapterÉ bem simples!



O layout que usamos na tela principal possui um Gallery e uma ImageView. A imagem escolhida pelo Gallery será mostrada na ImageView, conforme vimos no evento de clique da tela principal.


Seção 7 - AutoCompleteTextView


O AutoCompleteTextView é um TextView editável que mostra sugestões completadas automaticamente enquanto o usuário está digitando. A lista de sugestões é mostrada em um menu dropdown de onde o usuário pode escolher um item para substituir o conteúdo do TextView.

O dropdown pode ser ocultado à qualquer momento, clicando-se no botão Back, ou, se nenhum item for selecionado no dropdown, pressionando a tecla Enter.

A lista de sugestões é obtida de um Adapter de dados, e aparece apenas após um dado número de caracteres digitados, definidos pela opção threshold.

Abaixo é mostrado nosso aplicativo de exemplo, que possui apenas uma tela principal e seu arquivo de layout em XML.

Eis a tela principal.



Podemos notar que na tela principal temos um evento de TextWatcher. Ele é chamado quando um texto é escolhido a partir da lista de opções dadas pelo AutoCompleteTextView. E podemos ver o que é implementado neste evento: o texto do TextView é mudado para o texto escolhido pela seleção do AutoCompleteTextView.

O layout em XML da tela principal segue abaixo.



O que podemos notar neste layout é a propriedade android:completionThreshold=”3”. Conforme mencionado no início desta seção 7, é o threshold que informa ao AutoCompleteTextView a quantidade de caracteres que devem ser digitados para que a pesquisa comece a ser realizada nos dados do Adapter. Logo, precisamos digitar ao menos 3 caracteres para que o AutoCompleteTextView pesquisa e popule o Adapter que contém os dados advindos de mItems.

Seção 8 - EndlessAdapter


Um EndlessAdapter é um Adapter que carrega mais dados quando o usuário atinge o fim da ListView. Este tipo de Adapter é útil quando temos um número bem grande de itens e não queremos mostrá-los (para evitar um tempo de carregamento de dados extenso).

A classe EndlessAdapter implementa o método getPendingView(). Este método trabalha como o tradicional método getView(), que recebe um parâmetro ViewGroup e retorna uma View. A principal diferença é que este método precisa retornar uma linha (View) que possa funcionar como uma espécie de buffer, indicando ao usuário que estão sendo buscados mais dados no background. Esta View não é cacheada pelo EndlessAdapter. Então, se você deseja reusá-la, o cache deve ser implementado por sua conta.

Se você usa o construtor que possui os argumentos Context e o ID com o ListAdapter, você pode evitar o uso do método getPendingView(), e o EndlessAdapter irá inflar o layout especificado que será necessário para criar este buffer. Este buffer gerenciará quais componentes de layout estarão visíveis: ora um TextView com os dados; ora uma ImageView com uma representação indicando que mais dados estão sendo buscados.

A classe EndlessAdapter também precisa implementar o método cacheInBackground(). Este método será chamado de uma thread paralela, e precisa buscar mais dados que serão eventualmente adicionados ao ListAdapter que usamos no construtor de classe. Em nosso exemplo, será simulada tal busca através de um comando sleep, executado por 10 segundos.

Este método retorna um Boolean, que retornará True se houver mais dados à serem buscados, e Falso caso contrário.

Por fim, a classe EndlessAdapter também precisa implementar o método appendCachedData(), que deve obter os dados cacheados por cacheInBackground() e deve anexá-los ao ListAdapter utilizado no construtor de classe. Enquanto o método cacheInBackground() é chamado por uma thread em paralelo, o método appendCachedData() é chamado na thread principal.

Em nosso exemplo de EndlessAdapter, temos uma tela principal, seu Adapter, o layout da tela principal e o layout dos dados da ListView que é inflada na tela principal. Eis a tela principal:



Podemos notar que nesta tela tudo de relevante que temos é a inicialização do Adapter. O que temos de diferente neste Adapter é o método getLastNonConfigurationInstance(), além do Adapter estar sendo populado com 25 itens (caso o Adapter seja nulo), ou então uma animação estar sendo chamada (caso o Adapter não seja nulo).

Agora iremos explicar as diferenças citadas no parágrafo acima: o método getLastNonConfigurationInstance() obtém a lista anteriormente inflada pelo EndlessAdapter. E a lógica arbitrada caso o Adapter seja ou não nulo pode ser facilmente entendida se o conceito do EndlessAdapter for entendido.

Repare que caso o Adapter seja nulo, populamos a lista com 25 itens. Caso não seja, uma animação é chamada, sinalizando que mais itens estão sendo carregados. E de fato, estão sendo mesmo. No EndlessAdapter a lista é mostrada de pouco em pouco. No caso do nosso exemplo, são mostrados 25 itens, e depois mais 25, e depois mais 25. E entre eles temos uma animação sinalizando que mais itens serão mostrados.

O EndlessAdapter não é implementado nativamente em Android. Ele é uma funcionalidade implementada por programadores não associados à Google, e disponibilizado em forma de biblioteca.

No exemplo que estamos explicando, para que o EndlessAdapter funciona, precisamos incluir a biblioteca EndlessAdapter nas propriedades do projeto. O conceito de biblioteca será melhor explicado nos próximos posts do Blog.

Para entender melhor o EndlessAdapter, vamos dar uma olhada na implementação de seu Adapter:



De longe, este é o Adapter mais complexo já visto neste post. Ele não segue os padrões de Adapter implementados nativamente pelo Android. Nele temos métodos como getPendingView(), cacheInBackground(), appendCachedData() e startProgressAnimation().

Podemos notar que no construtor do Adapter é inicializada uma RotateAnimation. Essa animação será usada pelo atributo mPendingView, que é chamada alternadamente, ora quando temos dados à serem populados na lista, ora quando todos os dados de um conjunto foram mostrados e queremos que mais dados sejam mostrados. Neste caso, a animação é executada, dando ao usuário a sensação de que os dados estão sendo carregados. Esta lógica é implementada no método getPendingView().

Já os métodos cacheInBackground() e appendCachedData() são responsáveis por gerenciar a adição dos dados na lista. Conforme podemos notar, são adicionados 75 itens, em conjuntos de 25 itens cada. Logo, será mostrada a animação do atributo mRotate exatas 2x.

Para entender melhor a lógica deste exemplo, usaremos um Log no início de cada método do nosso aplicativo.

Ao abrirmos o aplicativo, eis o que o Log nos diz:
[EndlessAdapterActivity].[getLastNonConfigurationInstance].
[DemoAdapter].[DemoAdapter].

Ao baixarmos a lista até o 25º item, o Log é atualizado:
[DemoAdapter].[getPendingView].
[DemoAdapter].[startProgressAnimation].
[DemoAdapter].[cacheInBackground].
[DemoAdapter].[appendCachedData].

Ou seja, primeiramente é acessado o método getPendingView(), onde é alternado o que é mostrado ao usuário (é ocultado o TextView e é mostrada a ImageView com a imagem de um círculo de flechas).

Secundariamente, é inicializada a animação que faz com que este círculo de flechas fique girando por 10 segundos, dando ao usuário a sensação de que os dados estão sendo carregados. E, por fim, os métodos cacheInBackground() e appendCachedData() são executados, e com eles são populados mais 25 itens na nossa lista. Esse ciclo se repete até que todos os tenham sido populados.

A tela principal possui o seguinte layout (uma lista simples):



Já cada elemento da lista possui um layout bem particular:



Entender este layout é vital para entender o conceito de EndlessAdapter. Repare que temos 2 componentes de interface neste layout. Porém, apenas um deles será mostrado por vez. Quando estamos visualizando os dados da lista, o TextView será mostrado. Quando já visualizamos todos os elementos da primeira porção de dados da lista, então a ImageView é mostrada (e a animação RotateAnimation é executada). E esta sequência é repetida até que todos os dados da lista sejam visualizados.

O entendimento deste Adapter não é muito simples. Porém, seu uso é extremamente útil, pois ao utilizar conjuntos de amostras dos dados da lista (e não a lista toda), além de ser utilizado menos memória e processamento, também temos uma interface mais amigável com o usuário.

Seção 9 - CustomAdapter


Por fim, o último Adapter que veremos é um Adapter que customizamos totalmente. Afinal, este é o principal objetivo deste post: aprendermos como criar um Adapter que satisfaça nossas necessidades.

Em nosso exemplo, criaremos uma classe chamada Unity, e criaremos um Adapter que infla este tipo de dado. Nosso exemplo possui 4 pacotes: activity, adapter, data e model.

Na classe model temos o tipo de dado que criamos:



No pacote data, temos os dados que serão populados na lista de Unity que usaremos no aplicativo.



Na classe adapter temos o nosso Adapter:



E a tela principal infla este Adapter:



E o layout de cada item da ListView que o arquivo de XML main.xml implementa é mostrado abaixo:



-----

E com tudo que foi mostrado acima, consigamos criar nosso adapter customizado sem grandes dificuldades. Porém, com tudo que foi visto neste post, nosso Adapter poderia ter sido criado de forma mais otimizada.

De que maneira isto poderia ter sido feito, deixo ao cargo do leitor deste Blog, em forma de um desafio.

Caso alguém tenha uma dúvida, crítica ou sugestão, sinta-se à vontade.

7 comentários:

  1. Rodrigo, dei uma olhada no GridView e, ao testar com scroll, os items estão trocando de posição

    ResponderExcluir
  2. Wendell, este erro acaba de ser corrigido. Obrigado pela crítica construtiva!

    ResponderExcluir
  3. Foi o Artigo mais completo que achei na internet sobre Adapters. Obrigado

    ResponderExcluir
  4. Obrigado pelo elogio, André da Silva!

    ResponderExcluir
  5. Este comentário foi removido pelo autor.

    ResponderExcluir
  6. Muito bom o artigo, bem organizado e facil de entender. Rodrigo voce teria algo sobre eu inflar um widget YoutubePlayerView ao inves de uma Imagem ?

    ResponderExcluir
  7. Ola tudo bem !Otimo poster!Poderia me tirar uma duvida ?Como faço para criar um adapter generico .onde eu o usaria para varias activitys.Tem como?

    ResponderExcluir