Listas e Adapters no Android
Este artigo apresentado uma série de cenários sobre o uso de listas e adapters onde os dados apresentados em tela serão originados de diferentes fontes de informação.
Guia do artigo:
- ListView
- Recuperação dos elementos da lista
- ArrayAdapter
- ListActivity
- Utilizando lista para apresentar dados de um banco de dados
- Utilizando campos de entrada em elementos da lista
- Adapters
Listas são uma categoria de componentes sofisticados da plataforma Android. Elas trabalham apresentando um conjunto de dados, muitas vezes formados por Strings (mas nada impede de termos imagens ou até mesmo outros componentes visuais), em listas para o usuário. Estes dados podem ser recuperados de listas estáticas como as definidas em um arquivo xml, ou de forma dinâmica, como o conteúdo originado de um banco de dados.
Estes componentes diferem dos componentes tradicionais (EditText, TextView, Button, etc.) pela forma da apresentação dos dados que são formados por nenhum, um ou vários elementos. Destacam-se nesta categoria os componentes ListView, Spinner e AutoCompleteTextView.
Para exemplificar o uso destes componentes de lista, bem como sua personalização a partir de Adapters, serão desenvolvidos alguns exemplos utilizando o componente ListView. Ao longo do artigo será apresentada a utilização de listas com:
- Elementos estáticos vindos de um arquivo xml;
- Elementos estáticos originados de um array de String;
- Elementos dinâmicos vindos de um banco de dados;
- Apresentação de uma quantidade diferente de dados na tela;
- Utilização de campos de entrada nos elementos da lista;
- Utilização de botões em elementos da lista.
Como primeiro passo devemos criar um projeto chamado UsandoListaAdapters com nome de tela principal activity_principal.xml e código Java com nome ActivityPrincipal.java. Os fontes deste artigo serão desenvolvidos utilizando Android Studio.
O componente ListView
O componente ListView é utilizado para apresentar uma grande quantidade de dados em formato de lista. Semelhante aos componentes de lista de outras plataformas, o ListView permite tratar eventos de cliques e de seleção, mas pode ser utilizado somente para apresentar dados (nesta situação, não é necessário o tratamento de eventos).
Para apresentar algumas das funcionalidades deste componente, vamos criar uma lista com todos os países da copa do mundo de 2014. Ao clicarmos em um dos nomes, mostraremos a posição da seleção no ranking da FIFA em um componente Toast.
Pode-se utilizar duas metodologias para o desenvolvimento deste exemplo:
- Recuperar os elementos da lista diretamente de um arquivo xml;
- Montar os elementos da lista em um componente ArrayAdapter a partir de um array de String.
Recuperação dos elementos da lista diretamente de um arquivo xml
Primeiramente vamos alterar o arquivo activity_principal.xml para inserir o componente ListView. Veja seu conteúdo na Listagem 1.
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/lvSelecoesCopa"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:entries="@array/paises_copa" />
Como o componente ListView é utilizado como o único componente de tela sem dividi-la com outros componentes visuais, costuma-se colocá-lo sem a necessidade de um gerenciador de layout. Isto pode ser observado na Listagem 1, onde na linha 01 já aparece a declaração do ListView e ao seu lado, na linha 02, outras informações referentes ao schema do xml. Porém, se houver a necessidade, o ListView pode ser utilizado em conjunto com outros componentes visuais na mesma tela sem nenhum problema.
É definido que a lista ocupará todo o espaço disponível na horizontal (linha 04) e vertical (linha 05). Temos também a propriedade android:entries (linha 06). Esta informa um array de String criado a partir de um arquivo xml que definirá os valores dos elementos do ListView. Ao codificar esta linha no editor de código, um erro é apresentado e só será corrigido após o array paises_copa ser criado.
Para criar um xml com esse array de String, clique com o botão direito sobre o projeto escolhendo a opção New – XML – Values XML File. Na primeira tela é solicitado o nome do arquivo xml (que pode ser elemento_lista).
No arquivo criado dentro da pasta values do projeto, deve-se editar o xml colocando o conteúdo conforme a Listagem 2.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="paises_copa">
<item >Alemanha</item>
<item >Argentina</item>
<item >Holanda</item>
<item >Colombia</item>
<item >Belgica</item>
<item >Uruguai</item>
<item >Brasil</item>
</string-array>
</resources>
No arquivo xml é possível verificar o nome da estrutura string-array na linha 03, assim como todos os elementos que o compõe (das linhas 04 a 10). A referência a este arquivo que acontece na linha 06 da Listagem 1 obedece a seguinte regra: todo o recurso existente no Android (conteúdo da pasta res) é referenciado via xml pelo caractere arroba (@), na sequência é informada a pasta ou tipo de recurso que neste caso é um array. Por fim, temos o nome do recurso definido como é paises_copa (por isso o android:entries=”@array/paises_copa”).
Feito isso, basta declarar e recuperar o componente ListView na classe Activity para um uso futuro da mesma (conforme apresentado na Listagem 3).
O código desenvolvido possui importação de classes, declaração dos componentes visuais e sua recuperação com o método findViewById() (linha 17).
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
public class PrincipalActivity extends Activity {
private ListView lvSelecoesCopa;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_principal);
lvSelecoesCopa = (ListView) findViewById( R.id.lvSelecoesCopa );
lvSelecoesCopa.setOnItemClickListener( new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
tratarOpcoesItem( arg2 );
}
} );
}
protected void tratarOpcoesItem( int posicao ) {
int posFifa = posicao + 1;
Toast.makeText( this, "Posição no Ranking: " + posFifa , Toast.LENGTH_LONG ).show();
}
}
Para registrar um evento de clique, deve-se atentar que o evento que interessa para a lista é o clique sobre um item: setOnItemClickListener() (linha 19). Assim como acontece tradicionalmente para o tratamento de clique de botão, aconselha-se utilizar uma classe interna anônima, que para o tratamento de clique no item é AdapterView.OnItemClickListener. Esta classe requer a codificação do método onItemClick (linha 22). Este método traz uma série de parâmetros, destacando-se o índice do elemento selecionado que no exemplo é apresentado com nome arg2.
Ao selecionar um elemento na lista, o método tratarOpcoesItem() é chamado – linha 24. Este método encontra-se codificado na linha 31 e sua função é recuperar a posição do elemento selecionado na lista, ou seja, seu índice (lembrando que o primeiro elemento da lista possui índice 0) e incrementar um no índice (linha 33). Esta informação é apresentada para o usuário em um componente Toast (linha 35). O resultado desta execução é apresentado na Figura 1.
No exemplo apresentado, o elemento Brasil foi selecionado e, por isso, na parte inferior da tela é apresentada sua posição no ranking da Fifa.
A apresentação de itens em uma lista utilizando elementos estáticos vindos de um arquivo xml torna-se útil quando deseja-se apresentar dados que não mudam ao longo do tempo como, por exemplo, as opções de um menu. Se for necessário um pouco mais de flexibilidade nos elementos desta lista, talvez seja necessário retornar os valores a partir do código Java.
Montagem dos elementos da lista em um componente ArrayAdapter
Neste segundo exemplo de uso de um ListView, o conteúdo apresentado no componente será recuperado diretamente do código Java. Assim, agora não faremos mais uso do arquivo elemento_lista.xml.
Inicialmente, deve-se modificar o activity_principal.xml (interface gráfica) retirando a propriedade android:entries (ver Listagem 4).
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/lvSelecoesCopa"
android:layout_width="match_parent"
android:layout_height="match_parent" />
A maior diferença acontecerá na classe Activity, já que nesta situação os elementos da lista serão valorizados em tempo de execução. A classe PrincipalActivity.java modificada é apresentada na Listagem 5. O código apresentado é muito parecido com o da Listagem 3, diferenciando apenas pelas linhas 20 a 26 que foram incluídas. Estas linhas são responsáveis por criar um array de String em tempo de execução e definir o conteúdo do ListView com estas informações.
Para isso, na linha 20 é instanciada uma lista de String com o nome das seleções. Embora neste exemplo a lista possua valores estáticos, na verdade poderia apresentar qualquer valor, dados que vêm do banco, digitados pelo usuário, entre outros.
A linha 23 instancia um ArrayAdapter. Este é um objeto que representa o conteúdo que será apresentado no ListView. Seu construtor espera, por parâmetro:
- O contexto do Adapter: neste caso, o próprio Activity;
- O layout para apresentação dos elementos da tela: foi utilizado um layout interno do Android. Assim, neste exemplo, o layout está armazenado no objeto Android: android.R.layout.simple_list_item_1;
- O array de String que define os elementos da lista.
Por fim, é definido o ArrayAdapter ao componente ListView (linha 26). O resultado da execução é o mesmo apresentado na Figura 1.
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
public class PrincipalActivity extends Activity {
private ListView lvSelecoesCopa;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_principal);
lvSelecoesCopa = (ListView) findViewById( R.id.lvSelecoesCopa );
String itens[] = { "Alemanha", "Argentina", "Holanda", "Colombia",
"Belgica", "Uruguai", "Brasil" };
ArrayAdapter<String> adapter = new ArrayAdapter<String> ( this,
android.R.layout.simple_list_item_1, itens );
lvSelecoesCopa.setAdapter( adapter );
lvSelecoesCopa.setOnItemClickListener( new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
tratarOpcoesItem( arg2 );
}
} );
}
protected void tratarOpcoesItem( int posicao ) {
int posFifa = posicao + 1;
Toast.makeText( this, "Posição no Ranking: " + posFifa , Toast.LENGTH_LONG ).show();
}
}
Este é o primeiro exemplo onde se apresenta um Adapter. Esta classe tem a função de personalizar a apresentação de dados em lista. Existem alguns exemplos de Adapters como ArrayAdapter (apresentação de dados em Lista com origem de um array), SimpleCursorAdapter (os dados vêm de um banco de dados), entre outros.
Outra característica é que o usuário pode criar seu próprio Adapter, apresentando dados diferentes na lista como imagens ou campos de entrada. O Adapter deixa as informações de uma lista “adaptáveis”, sendo facilmente personalizadas.
É possível também, em tempo de execução, recuperar o conteúdo de um arquivo xml apresentando-o em um ListView. Basta criar um ArrayAdapter com o código apresentado a seguir valorizando a propriedade adapter de um ListView:
ArrayAdapter<CharSequence> adapter =
ArrayAdapter.createFromResource(this,R.array.paises_copa, android.R.layout.simple_list_item_1 );
lvSelecoesCopa.setAdapter( adapter );
Desta forma, não seriam necessárias as linhas 20 a 26 da Listagem 5.
Uso de um ListActivity no lugar da interface xml
Uma outra forma de usar o ListView é utilizando a classe ListActivity. Nesta situação, não é necessário o arquivo activity_principal.xml. A diferença acontece na declaração da Activity Java, substituindo o extends Activity por extends ListActivity e tirando toda a referência ao ListView conforme apresentado na Listagem 6.
Ao comparar este código com o da Listagem 5 observa-se que se diminui consideravelmente o número de linhas de código assim como a complexidade da classe. Também vale lembrar que, para este exemplo, o arquivo activity_principal.xml pode ser excluído.
Além da diferença na herança que acontece na linha 08, outra diferença está na ausência de uma classe ListView já que a própria classe é uma lista. Assim, toda vez que se precisa referenciar o ListView, basta utilizar a palavra reservada this como pode ser observado no setListAdapter() da linha 21.
Outra diferença é a ausência do comando setContentView(). Já que não existe uma interface xml para a tela, o próprio ListActivity cria e gerencia um ListView ocupando a tela inteira do device.
import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
public class PrincipalActivity extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String itens[] = { "Alemanha", "Argentina", "Holanda", "Colombia",
"Belgica", "Uruguai", "Brasil" };
ArrayAdapter<String> adapter = new ArrayAdapter<String> ( this,
android.R.layout.simple_list_item_1, itens );
this.setListAdapter( adapter );
}
@Override
protected void onListItemClick(ListView l, View v, int posicao, long id) {
int posFifa = posicao + 1;
Toast.makeText( this, "Posição no Ranking: " + posFifa , Toast.LENGTH_LONG ).show();
}
}
Por fim, o tratamento do evento de elemento selecionado acontece a partir do método onListItemClick() (linha 26) que foi herdado da classe pai. Após a execução, o resultado visual também permanece o mesmo da Figura 1.
É possível usar a classe ListActivity e mesmo assim utilizar uma interface gráfica xml com a declaração de ListView. Esta técnica é muito utilizada se o programador precisa definir características da lista diretamente no arquivo xml. Neste caso, é obrigatório que o ListView definido na interface gráfica possua o nome android:list, como pode ser observado no código a seguir:
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/android:list"
android:layout_width="match_parent" android:layout_height="match_parent"/>
Utilizando lista para apresentar dados de um banco de dados
No exemplo que iremos desenvolver, optamos em alimentar o banco de dados com as informações vindas do GPS do device, apresentando-as posteriormente em uma lista. Assim, o aplicativo irá, basicamente, ler informações de um GPS, armazená-las e, em seguida, apresentá-las através de uma lista para o usuário.
Para o exemplo será utilizado o mesmo projeto – UsandoListaAdapters, assim como os mesmos arquivos: activity_principal.xml e PrincipalActivity.java. Esta tela será responsável por recuperar os dados do GPS e armazená-los em um banco de dados, atualizando em alguns TextViews as coordenadas geográficas (latitude e longitude). Desta forma, o primeiro passo é modificar a interface gráfica (activity_principal.xml) conforme código apresentado na Listagem 7.
A interface gráfica do aplicativo organiza os componentes visuais de forma linear (linha 01) na orientação vertical (linha 05). Após isso, pares de componentes são declarados, sendo TextViews que serão utilizados como rótulos das informações (linhas 08, 19 e 30) e os TextViews que apresentarão os dados (linhas 13, 24 e 35). As informações apresentadas na tela serão a latitude, longitude e a precisão dos dados. Por fim, temos um botão listar (linha 41) que apresentará os dados armazenados no banco em uma lista.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".PrincipalActivity">
<TextView
android:text="Latitude:"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvLatitude"
android:text="0.0"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="Longitude:"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvLongitude"
android:text="0.0"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="Precisão:"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvPrecisao"
android:text="0.0"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btListar"
android:text="Listar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="btListarOnClick" />
</LinearLayout>
O passo seguinte é codificar a classe Java responsável pelo processamento da tela principal. Seu código é apresentado nas Listagens 8 e 9.
O código apresentado declara na linha 19 a Activity que possui como característica a implementação da interface LocationListener. Essa lhe torna apta a processar as informações recebidas do GPS.
Das linhas 21 a 23 são declarados os objetos correspondentes aos componentes visuais, assim como o objeto que representa o banco de dados SQLite (linha 25).
No método onCreate() – linha 28, além dos comandos para iniciar a Activity e apresentar a tela para o usuário (linhas 29 e 30), é criado/recuperado um banco de dados SQLite (linha 32) com o nome de "db". Também é executado o comando para criação da tabela posição que terá o campo chave primária _id do tipo Integer, latitude e longitude do tipo double, e o campo data_hora do tipo texto, que armazenará o momento da leitura.
O próximo passo é recuperar os componentes visuais (linhas 38 a 40) e o serviço que permite a recepção dos códigos do GPS (linha 42). Após recuperar o objeto, o mesmo é testado para verificar se o recurso de GPS está habilitado no device em execução (linha 45). Caso negativo, um AlertDialog (linha 47) é apresentado questionando ao usuário se o mesmo deseja ligar o GPS. Se a resposta for positiva (linha 50), a interface de ativação do GPS no device é apresentada (linha 53), caso contrário (linha 58) o AlertDialog é retirado da tela (linha 61).
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.provider.Settings;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PrincipalActivity extends Activity implements LocationListener {
private TextView tvLatitude;
private TextView tvLongitude;
private TextView tvPrecisao;
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_principal);
db = this.openOrCreateDatabase( "db", Context.MODE_PRIVATE, null );
db.execSQL( "CREATE TABLE IF NOT EXISTS posicao ( " +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, latitude DOUBLE, " +
"longitude DOUBLE, data_hora TEXT)");
tvLatitude = (TextView) findViewById( R.id.tvLatitude );
tvLongitude = (TextView) findViewById( R.id.tvLongitude );
tvPrecisao = (TextView) findViewById( R.id.tvPrecisao );
LocationManager lm = (LocationManager)
this.getSystemService(Context.LOCATION_SERVICE );
if ( ! lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ) {
AlertDialog.Builder alerta = new AlertDialog.Builder( this );
alerta.setTitle( "Atenção" );
alerta.setMessage("Recurso de GPS desativado. Deseja ativar?");
alerta.setPositiveButton("Ativar", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
PrincipalActivity.this.startActivity(intent);
}
});
alerta.setNegativeButton( "Cancelar", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
} );
alerta.show();
}
lm.requestLocationUpdates( LocationManager.GPS_PROVIDER,
0, 0, this );
}
Por fim, ainda na Listagem 8, é registrada a intenção de leitura dos dados do GPS (linha 69). Em seguida, já na Listagem 9, esses dados serão capturados pelos quatro métodos da interface LocationListener que são onLocationChanged (linha 75), onStatusChanged (linha 98), onProviderEnabled (linha 103) e onProviderDisabled (linha 108).
Para o aplicativo em questão, apenas o método onLocationChanged() foi codificado, sendo que este recupera os dados necessários (latitude, longitude e precisão, respectivamente nas linhas 76, 77 e 78), assim como apresenta-os na tela para o usuário (linhas 80 a 82).
Como no banco de dados o momento desta recuperação também é necessário, um objeto do tipo java.util.Date é instanciado (linha 84), assim como um formatador que deixará o dado no formato dia/mes/ano – hora:minuto:segundo (linha 85).
@Override
public void onLocationChanged(Location location) {
double latitude = location.getLatitude();
double longitude = location.getLongitude();
double precisao = location.getAccuracy();
tvLatitude.setText( String.valueOf( latitude ) );
tvLongitude.setText( String.valueOf( longitude ) );
tvPrecisao.setText( String.valueOf( precisao ) );
Date agora = new Date();
SimpleDateFormat formatador = new SimpleDateFormat( "dd/MM/yyyy - hh:mm:ss" );
if ( precisao <= 10 ) {
ContentValues registro = new ContentValues();
registro.put("latitude", latitude);
registro.put("longitude", longitude);
registro.put("data_hora", formatador.format(agora));
db.insert("posicao", null, registro);
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
public void btListarOnClick( View v ) {
Intent i = new Intent( this, ListaPosicoesActivity.class );
startActivity( i );
}
}
Como não existe garantia de precisão nos dados, são ignorados aqueles menos precisos (linha 87), sendo armazenados os demais na tabela posição (linha 88 a 93). Vale lembrar que o campo _id não precisa ser valorizado uma vez que ele é do tipo AUTOINCREMENT.
Por fim, para concluir a interface gráfica, o método btListarOnClick() é codificado (linha 112). Este é responsável por apresentar uma nova tela na qual os dados armazenados no banco serão apresentados em uma lista. Para isso, uma nova Activity deve ser codificada com nome ListaPosicoesActivity.java.
O processo para criar uma Activity nova é clicar com o botão direito sobre o projeto escolhendo a opção new – Activity – Blank Activity. O código da classe ListaPosicoesActivity.java é apresentado na Listagem 10.
Inicialmente serão apresentadas apenas as informações correspondentes aos códigos latitude e longitude (dois campos por registro). Para o exemplo, optou-se por uma classe que implementa internamente uma lista – ListActivity.
Para a recuperação dos dados do SQLite, um objeto que representa o banco é declarado (linha 15) e instanciado (linha 21) utilizando o método openOrCreateDatabase. Como os dados da lista serão recuperados do banco, um objeto Cursor é necessário para armazenar o resultado da pesquisa (linha 23).
import android.app.ListActivity;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;
public class ListaPosicoesActivity extends ListActivity {
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
db = openOrCreateDatabase( "db", Context.MODE_PRIVATE, null );
Cursor registros = db.query( "posicao", null, null, null, null, null, null );
SimpleCursorAdapter adapter = new SimpleCursorAdapter( this,
android.R.layout.simple_list_item_2,
registros,
new String[] { "longitude", "latitude" },
new int[] { android.R.id.text1, android.R.id.text2 }
);
setListAdapter( adapter );
}
}
É importante ressaltar que todos os dados presentes neste objeto serão apresentados para o usuário. Desta forma, caso desejemos uma classificação (order by), um critério de pesquisa (where), agrupamento (group by), assim como fórmulas matemáticas ou critérios, estas devem ser definidas na instanciação da classe Cursor. A partir deste ponto, os dados recuperados serão apresentados na lista.
Para o exemplo, foram recuperados todos os campos da tabela posicao sem utilização de critério de seleção ou classificação. Para apresentação deste conjunto de dados na tela, o Adapter SimpleCursorAdapter é necessário.
Este Adapter tem por característica mostrar dados compostos na tela de um ListView. Estes dados têm origem de um Cursor, que comumente é utilizado para armazenar o resultado de uma pesquisa no banco SQLite. Contudo, ele também poderia armazenar o resultado de outros tipos de pesquisa como os contatos de um device Android.
Na instanciação do SimpleCursorAdapter da linha 25, foi utilizada uma interface padrão disponível no Android a qual suporta dois campos no elemento da lista. Este é semelhante ao apresentado na Listagem 5, onde foram apresentadas as seleções de futebol no ranking da Fifa. Naquele exemplo, foi definido um ArrayAdapter (linha 23) já que os dados vinham de um Array de String, e foi definido nesta mesma linha o layout android.R.layout.simple_list_item_1, já que se tratava de uma única informação por elemento da lista.
Já neste exemplo, como são duas informações (latitude e longitude), foi utilizado o layout android.R.layout.simple_list_item_2. O terceiro parâmetro do SimpleCursorAdapter é a origem dos dados, que para o exemplo é o Cursor chamado registros. A quarta informação é um array de string com o nome dos campos que serão apresentados na tela e que vêm do banco de dados (os campos longitude e latitude). Por fim, o último parâmetro é um array de inteiro que terá o nome dos campos na tela que receberão estes dados. Como se trata de uma interface definida pelo próprio Android, no simple_list_item_2 existem dois campos: o android.R.id.text1, que tem um destaque maior na lista, e o android.R.id.text2, este com destaque menor.
Por fim, só precisamos definir este adapter como sendo o Adapter da classe e executar o programa. O resultado da execução é apresentado na Figura 2.
Como pode ser observado, a tela da esquerda é a tela principal do aplicativo com os dados referentes a latitude e longitude simulados. Ao clicar no botão Listar, a tela da direita é apresentada. Para exemplificar, três leituras do GPS foram simuladas, entretanto, observa-se que a disposição dos componentes na tela não é a melhor para esta situação. Para o exemplo, como a longitude foi definida sendo o text1 do layout simple_list_item_2, ela é apresentada em destaque. Já a latitude é apresentada com menos evidência. O ideal é que ambas as informações fossem apresentadas da mesma maneira e, em destaque, o momento da leitura.
Infelizmente, existem poucos layouts prontos disponíveis. Neste artigo tratamos o simple_list_item_1 (Listagem 5) e simple_list_item_2 (Listagem 10). Assim, um layout personalizado é necessário para este exemplo.
Para criar um layout personalizado, deve-se criar um arquivo de layout xml. Para isso, clica-se com o botão direito sobre a pasta layout escolhendo a opção New – XML – Layout XML File. Será solicitado o nome do arquivo, que para o exemplo será elemento_lista.xml.
Este arquivo de layout não terá um Activity associado a ele, já que se trata do layout de elemento de uma lista e não de uma tela como um todo. Entretanto, as regras para criar este layout são idênticas aos layouts de telas, podendo ser utilizado qualquer componente visual ou gerenciador de layout. O código do arquivo elemento_lista.xml é apresentado na Listagem 11.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/dataHoraLista"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Latitude:" />
<TextView
android:id="@+id/tvLatitudeLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" Longitude:" />
<TextView
android:id="@+id/tvLongitudeLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
Esta interface possui um componente em destaque para a apresentação da data e da hora (linha 07). Possui também dois pares de componentes, dois rótulos para apresentação dos dados (linhas 18 e 28) e dois componentes para os dados (linhas 23 e 33). Estes componentes são organizados um ao lado do outro, por isso estão dentro de um LinearLayout horizontal (linha 13).
Observa-se que os componentes de apresentação de dados possuem nomes, sendo dataHoraLista (linha 08), tvLatitudeLista (linha 24) e tvLongitudeLista (linha 34).
Para apresentação deste novo layout, basta mudar a declaração do SimpleCursorAdapter substituindo a tela android.R.layout.simple_list_item_2 para a interface desenvolvida (R.layout.elemento_list). Além disso, devemos mudar a origem dos dados que passa a ser os três campos: latitude, longitude e data_hora, e o destino, que deixa de ser o android.R.id.text1 e android.R.id.text2 e passa a ser o R.id.tvLatitudeLista, R.id.tvLongitudeLista e R.id.dataHoraLista. Estas modificações podem ser observadas na Listagem 12.
import android.app.ListActivity;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;
public class ListaPosicoesActivity extends ListActivity {
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
db = openOrCreateDatabase( "db", Context.MODE_PRIVATE, null );
Cursor registros = db.query( "posicao", null, null, null, null, null, null );
SimpleCursorAdapter adapter = new SimpleCursorAdapter( this,
R.layout.elemento_lista,
registros,
new String[] { "longitude", "latitude", "data_hora },
new int[] { R.id.tvLongitudeLista, R.id.tvLatitudeLista, R.id.dataHoraLista }
);
setListAdapter( adapter );
}
}
O resultado desta mudança é apresentado na Figura 3. Como pode ser observado, a tela é apresentada de uma forma mais coerente se considerarmos os dados armazenados. Desta vez é apresentado em destaque o momento da gravação dos dados, assim como a latitude e a longitude (estes dois últimos campos com rótulos).
Utilizando campos de entrada em elementos da lista
Assim como as listas são úteis para apresentar dados para o usuário, elas também podem ser úteis para ler dados do usuário. Imagine um aplicativo onde o usuário tem que informar um dado para cada registro de um banco: ou o aplicativo apresenta uma nova tela para digitação dos dados de cada elemento, ou apresenta uma série de campos de entrada de dados, um para cada registro do banco. Sem dúvida, a segunda opção é muito mais simples e funcional.
Para exemplificar o uso de campos de entrada em elementos do banco, vamos adicionar na tela um EditText abaixo dos dados recuperados do banco (data_hora, latitude e longitude), onde o usuário poderá digitar uma descrição para aquela posição para, futuramente, poder armazená-la no banco de dados.
O primeiro passo para esta mudança é adicionar no arquivo elemento_lista.xml o campo EditText. O novo layout do arquivo é apresentado na Listagem 13.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/dataHoraLista"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Latitude:" />
<TextView
android:id="@+id/tvLatitudeLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" Longitude:" />
<TextView
android:id="@+id/tvLongitudeLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent "
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Descrição:"/>
<EditText
android:id="@+id/etDescricaoLista"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"/>
</LinearLayout>
</LinearLayout>
A diferença do código em relação à versão anterior está na inclusão de um Linear Layout ao final do elemento (linha 41). Este contém um TextView como rótulo para a descrição (linha 46) e um EditText para a digitação da descrição (linha 51).
Desta forma, ao executarmos o aplicativo, verificamos que agora todo elemento da tela já possui um campo de entrada de dados que pode ser preenchido (ver Figura 4).
O passo seguinte é recuperar estes campos para um futuro processamento. Para evitar uma complexidade desnecessária, optamos por apenas mostrar na tela, via Dialog, o conteúdo digitado nos campos de entrada de dados.
Toda a tela de aplicação Android inicia por padrão com um menu, sendo que muitas vezes este não é utilizado na aplicação. Neste caso, ambas as telas (Principal e ListaPosicoes) possuem menus, como pode ser observado na estrutura de pastas do projeto (Figura 5).
Dentro do arquivo menu_lista_posicoes.xml, foi definido automaticamente um menu de configurações (Settings). Vamos alterar este nome para Visualizar Entradas conforme Listagem 14.
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="br.edu.utfpr.rastreador.ListaPosicoesActivity">
<item
android:id="@+id/mnVisualizarEntradas"
android:title="Visualizar Entradas"
android:orderInCategory="100"
app:showAsAction="never" />
</menu>
Agora precisamos fazer o tratamento deste menu na tela de ListaPosicoes apresentando o menu para o usuário quando a tecla de menu do emulador for pressionado (ou a tecla de atalho F2), e também tratando o clique no menu Visualizar Entradas.
O código da classe ListaPosicoesActivity.java, já com o tratamento de menu, é apresentado na Listagem 15.A diferença do código apresentado em relação à Listagem 12 acontece a partir da linha 35. Como foi inserido um menu para tratamento na tela de lista, cabe à Activity criar este menu e apresentá-lo na tela. Isto é definido no arquivo menu_lista_posicoes.xml na Listagem 14.
Desta forma, na Listagem 15, no método onCreateOptionMenu (linha 36) é carregado o menu presente no arquivo xml. Já o método onOptionsItemSelected (linha 42) processa a interação com os itens de menu. Desta forma, inicialmente é recuperado o id do item clicado (linha 43) e em seguida, ele é comparado com o item desejado (linha 45) para só então realizarmos o processamento. Neste caso, será executado o método tratarCamposEntrada (linha 47).
Este método tem a função de recuperar a quantidade de elementos presentes na lista (linha 56) e criar uma mensagem que será apresentada para o usuário via componente Dialog (linha 58). Após isso, são percorridos todos os elementos da lista (linha 60) recuperando o componente EditText presente nele (linha 61 e 62). A partir deste ponto, é verificado se o conteúdo do EditText é diferente de cadeia vazia (linha 64). Feito isso, seu conteúdo é concatenado à String msgTela.
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.SimpleCursorAdapter;
public class ListaPosicoesActivity extends ListActivity {
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
db = openOrCreateDatabase( "db", Context.MODE_PRIVATE, null );
Cursor registros = db.query( "posicao", null, null, null, null, null, null );
SimpleCursorAdapter adapter = new SimpleCursorAdapter( this,
R.layout.elemento_lista,
registros,
new String[] { "longitude", "latitude", "data_hora" },
new int[] { R.id.tvLongitudeLista, R.id.tvLatitudeLista, R.id.dataHoraLista }
);
setListAdapter( adapter );
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_lista_posicoes, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.mnVisualizarEntradas) {
tratarCamposEntrada();
return true;
}
return super.onOptionsItemSelected(item);
}
private void tratarCamposEntrada() {
int qtd = getListAdapter().getCount();
String msgTela = "";
for ( int i = 0; i < qtd; i++ ) {
EditText et = (EditText)
getListView().getChildAt( i ).findViewById( R.id.etDescricaoLista );
if ( ! et.getText().toString().equals( "" ) ) {
msgTela += et.getText().toString() + "\n";
}
}
AlertDialog.Builder alerta = new AlertDialog.Builder( this );
alerta.setTitle( "Informação" );
alerta.setMessage( "Dados digitados: \n" + msgTela );
alerta.setNeutralButton( "OK", null );
alerta.show();
}
}
No final da rotina, um AlertDialg é criado para a apresentação destes dados na tela (linhas 70 a 74). O resultado da execução desta nova Activity é apresentado na Figura 6.
Entretanto, à medida que vamos aprofundando no estudo das listas, verificamos que podemos considerar um elemento de lista como uma pequena aplicação, havendo dentro dele um layout (criado na Listagem 11). Além disso, podemos também personalizar seu funcionamento com a apresentação de botões em elementos de lista ou campos de entrada.
Para minimizar estes problemas, assim como facilitar a personalização dos elementos de lista, é possível criar nossos próprios Adapters.
Criando Adapters personalizados
Todo Adapter disponível na plataforma Android é uma classe filha de BaseAdapter. Estes Adapters podem ser diferenciados pelo modo como serão apresentados os dados na lista, assim como pela origem dos dados.
Quanto mais complexo e personalizado um elemento da lista, maior é a necessidade da criação de um Adapter. Para exemplificar o uso de um Adapter personalizado, vamos adicionar na tela um componente de interação que permite que o usuário interaja com cada linha do ListView. Este componente será um Button e terá a função de excluir o registro do banco de dados.
O primeiro passo para esta mudança é adicionar no arquivo elemento_lista.xml o campo Button. O novo layout do arquivo é apresentado na Listagem 16.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/dataHoraLista"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Latitude:" />
<TextView
android:id="@+id/tvLatitudeLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" Longitude:" />
<TextView
android:id="@+id/tvLongitudeLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btExcluirLista"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Excluir"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Descrição:"/>
<EditText
android:id="@+id/etDescricaoLista"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"/>
</LinearLayout>
</LinearLayout>
A diferença em relação à versão anterior do código está na inclusão do botão btExcluirLista (linha 38). Este será apresentado ao lado dos dados da localização (latitude e longitude).
Como temos uma interface relativamente complexa nos elementos da lista formada por campos de saída de dados (TextView), entrada de dados (EditText) e processamento (Button), é mais do que justificável a utilização de uma classe para o tratamento deste elemento de lista. Esta classe se chamará PosicaoAdapter. Seu código é apresentado nas Listagens 17 e 18.
Uma classe Adapter, subclasse de BaseAdapter, possui um comportamento diferente do tratamento utilizado pelos Activities vistos até o momento. Esta classe tem como característica herdar funcionalidades de BaseAdapter e, assim, deve codificar seus quatro métodos abstratos: getCount(), getItem(), getItemId() e getView(). Além destes quatro métodos, na maioria das vezes é necessário um método construtor (linha 24). Este costuma receber por parâmetro o contexto (que possui informações sobre a aplicação), assim como uma lista com os registros que serão apresentados na lista. Para o exemplo, passamos por parâmetro o Cursor, que possui os registros do banco de dados que serão apresentados.
Estas duas informações são armazenadas em atributos da classe (linhas 25 e 26). Também foi optado por instanciar no método construtor um objeto do tipo LayoutInflater (linha 27). Este tem a função de manipular dados referentes a componentes visuais.
Por fim, a última linha do método construtor instancia um objeto do tipo SQLiteDatabase (linha 29), o qual permite realizar comandos no banco de dados. No nosso exemplo, a exclusão de registros neste.
Os próximos três métodos apresentados são relativamente simples. O método getCount(), linha 33, retorna à quantidade de registros apresentados na tela (linha 34). O método getItem(),linha 38, retorna o elemento em questão da lista. Já o método getItemId(), linha 43, retorna o id de um elemento da lista.
Quando se utiliza o tratamento de elementos de uma lista, o método mais importante é o getView (linha 48). Ele é responsável por formatar os dados de um elemento da lista, assim como tratar eventos gerados por este. Assim, o primeiro passo do método é recuperar a tela que possui os componentes visuais da lista, neste caso, o R.layout.elemento_lista (linha 49). A partir da instância desta tela, é possível recuperar seus componentes que são três TextView (linha 51, 54 e 57) - um para apresentar a latitude, o outro a longitude e um último para apresentar a Data e Hora. Além disso, temos um componente EditText (linha 60) o qual permite digitar uma descrição para a posição e o componente do botão excluir (linha 63).
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListAdapter;
import android.widget.TextView;
import android.widget.Toast;
import br.com.devmedia.usandolistaadapter.R;
public class PosicaoAdapter extends BaseAdapter {
private Context c;
private Cursor registros;
private LayoutInflater inflater;
private SQLiteDatabase db;
private int id;
public PosicaoAdapter( Context c, Cursor registros ) {
this.c = c;
this.registros = registros;
inflater = (LayoutInflater)
c.getSystemService( c.LAYOUT_INFLATER_SERVICE );
db = c.openOrCreateDatabase("db", Context.MODE_PRIVATE, null);
}
@Override
public int getCount() {
return registros.getCount();
}
@Override
public Object getItem(int position) {
return registros;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = inflater.inflate(R.layout.elemento_lista, null);
TextView tvLatitudeLista = (TextView)
v.findViewById(R.id.tvLatitudeLista);
TextView tvLongitudeLista = (TextView)
v.findViewById(R.id.tvLongitudeLista);
TextView tvDataHoraLista = (TextView)
v.findViewById(R.id.dataHoraLista);
EditText etDescricaoLista = (EditText)
v.findViewById(R.id.etDescricaoLista);
Button btExcluirLista = (Button)
v.findViewById(R.id.btExcluirLista);
Já na Listagem 18, após recuperar a instância dos componentes visuais, é realizado o tratamento de clique do botão excluir (linha 65) o qual executa o método excluirRegistro() – linha 68. Para formatar os dados que serão apresentados no elemento da lista, o Cursor registros é posicionado para a posição definida pelo parâmetro position (linha 72). Havendo sucesso no posicionamento, os campos da tabela referentes aos registros são recuperados (id, latitude, longitude e data e hora, linhas 74 a 77). Por fim, estes campos são apresentados na tela nos elementos da lista (linhas 79 a 81).
Já o método excluirRegistro() – linha 88, tem a função de excluir o registro com id específico (linha 89), este representando o elemento da lista em que o botão foi pressionado. Feito isso, uma nova consulta é necessária com a função de atualizar o objeto registros (linha 90). Finalizando, é solicitada a atualização da lista na tela (linha 91) assim como a apresentação de uma mensagem informativa (linha 93).
btExcluirLista.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
excluirRegistro();
}
});
if (registros.moveToPosition(position)) {
id = registros.getInt(registros.getColumnIndex("_id"));
double latitudeBd = registros.getDouble(registros.getColumnIndex("latitude"));
double longitudeBd = registros.getDouble(registros.getColumnIndex("longitude"));
String dataHora = registros.getString(registros.getColumnIndex("data_hora"));
tvLatitudeLista.setText(String.valueOf(latitudeBd));
tvLongitudeLista.setText(String.valueOf(longitudeBd));
tvDataHoraLista.setText(dataHora);
}
return v;
}
private void excluirRegistro() {
db.delete( "posicao", "_id="+id, null );
registros = db.query( "posicao", null, null, null, null, null, null );
notifyDataSetChanged();
Toast.makeText( c, "Registro Excluído com sucesso!", Toast.LENGTH_LONG ).show();
}
}
Para finalizar o aplicativo, basta modificar a definição do Adapter na classe ListaPosicoesActivity.java, o qual acontece no método onCreate(). Ver Listagem 19. A única diferença deste código em relação ao código da Listagem 15 está na declaração do Adapter, que acontece na linha 24. Após esta mudança, basta executar o aplicativo e verificar a interação do usuários com os elementos da lista a partir do botão excluir, conforme Figura 7.
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.SimpleCursorAdapter;
public class ListaPosicoesActivity extends ListActivity {
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
db = openOrCreateDatabase( "db", Context.MODE_PRIVATE, null );
Cursor registros = db.query( "posicao", null, null, null, null, null, null );
PosicaoAdapter adapter = new PosicaoAdapter( this, registros );
setListAdapter( adapter );
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_lista_posicoes, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.mnVisualizarEntradas) {
tratarCamposEntrada();
return true;
}
return super.onOptionsItemSelected(item);
}
private void tratarCamposEntrada() {
int qtd = getListAdapter().getCount();
String msgTela = "";
for ( int i = 0; i < qtd; i++ ) {
EditText et = (EditText)
getListView().getChildAt( i ).findViewById( R.id.etDescricaoLista );
if ( ! et.getText().toString().equals( "" ) ) {
msgTela += et.getText().toString() + "\n";
}
}
AlertDialog.Builder alerta = new AlertDialog.Builder( this );
alerta.setTitle( "Informação" );
alerta.setMessage( "Dados digitados: \n" + msgTela );
alerta.setNeutralButton( "OK", null );
alerta.show();
}
}
O uso de listas em aplicações Android beneficia a usabilidade do aplicativo. Através delas é possível ter uma quantidade maior de informações em uma tela tornando sua aplicação ainda mais intuitiva. Isto porque elas permitem representar dados provenientes de diferentes origens e modificar a forma como elas é apresentada para o usuário através do uso de adapters.
Confira também
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo