Olá povo,
Eu comecei a escrever esse post na Campus Party Recife 2012. Palestrei sobre Android no evento, e apresentei um exemplo que faz uma busca no Twitter e exibe o resultado dessa pesquisa.
Entretanto, durante a palestra não deu para mostrar e resolver alguns problemas básicos que aplicação apresenta. São eles:
1) O Adapter que preenche a lista não foi implementado de forma eficiente;
2) Dar um feedback visual para o usuário enquanto está baixando informações;
3) Avisar ao usuário quando não houver conexão com a internet;
4) Fazer um tratamento caso tenha ocorrido algum problema durante o parse do JSON que é retornado pelo Twitter;
5) Tratar mudança de orientação para evitar que os dados sejam baixados novamente.
O objetivo desse post é mostrar como solucionar esses problemas. Mas antes de começar, vamos mostrar as duas classes básicas da aplicação: Tweet e BuscaTwitter.
public class Tweet {
String text;
String profile_image_url;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getProfile_image_url() {
return profile_image_url;
}
public void setProfile_image_url(String profile_image_url) {
this.profile_image_url = profile_image_url;
}
}
public class BuscaTwitter {
List<Tweet> results;
public List<Tweet> getResults() {
return results;
}
public void setResults(List<Tweet> result) {
this.results = result;
}
}
A primeira mapeia representará cada tweet postado pelo usuário com a hashtag #CPRecife. E a segunda representa a lista desses tweets. Criei essas classes para utilizar a biblioteca GSON que comentarei mais adiante. Vamos agora a lista das soluções.
Solução 1
A classe TweetAdapter utiliza o padrão Adapter para preencher as informações do componente visual de lista. Diferentemente da versão criada na palestra, a versão abaixo é implementada de forma eficiente.
public class TweetAdapter extends ArrayAdapter<Tweet> {
public TweetAdapter(
Context context, List<Tweet> objects) {
super(context, 0, 0, objects);
}
public View getView(
int position, View convertView, ViewGroup parent) {
ViewHolder holder;
Tweet tweet = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(
getContext()).inflate(
R.layout.linha_tweet, null);
holder = new ViewHolder();
holder.txtTexto = (TextView)
convertView.findViewById(R.id.textView1);
holder.imgFoto = (ImageView)
convertView.findViewById(R.id.imageView1);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.txtTexto.setText(tweet.getText());
BitmapManager.getInstance().loadBitmap(
tweet.getProfile_image_url(),
holder.imgFoto);
return convertView;
}
static class ViewHolder {
ImageView imgFoto;
TextView txtTexto;
}
}
Não entendeu essa classe? Clique
aqui.
O arquivo de layout usado pelo adapter é mostrado abaixo:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Medium Text"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
No exemplo acima, estou usando a classe BitmapManager que faz o download de forma assíncrona e por demanda da foto do perfil do usuário que postou o Tweet. Além disso, ela faz um cache em memória dessas imagens. Disponibilizei essa classe
aqui, mas se quiser melhora-la, você pode salvar as imagens no cartão de memória do aparelho.
Solucão 2, 3, 4 e 5 :)
As demais soluções são implementadas no Fragment e na AsyncTask. A Activity do projeto é mostrada abaixo.
public class ListTweetActivity
extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
setContentView(R.layout.activity_list_tweet);
}
}
A Activity acima, apenas exibe o arquivo de layout abaixo, que contém apenas um fragmento:
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
class="ngvl.android.cprecife.ListTweetFragment"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:tag="listTweets" />
A maior parte da solução é feita no fragmento e na AsyncTask descritas na classe abaixo. Não conhece fragments? Clique
aqui.
public class ListTweetFragment
extends ListFragment
implements OnClickListener {
private TweetAsyncTask asyncTask;
private List<Tweet> tweets;
private ProgressDialog dialog;
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View layout = inflater.inflate(
R.layout.fragment_list_tweet, container);
layout.findViewById(R.id.btnRefresh)
.setOnClickListener(this);
return layout;
}
@Override
public void onActivityCreated(
Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
if (asyncTask == null){
iniciarDownload();
} else if
(asyncTask.getStatus() == Status.RUNNING){
showDialog();
}
}
@Override
public void onDetach() {
super.onDetach();
if (dialog != null) dialog.dismiss();
}
public void onClick(View v) {
iniciarDownload();
}
private void showDialog() {
dialog = ProgressDialog.show(getActivity(),
"Aguarde", "Carregando tweets");
}
private void iniciarDownload(){
ConnectivityManager cm = (ConnectivityManager)
getActivity().getSystemService(
Context.CONNECTIVITY_SERVICE);
int wifi = ConnectivityManager.TYPE_WIFI;
int mobile = ConnectivityManager.TYPE_MOBILE;
if (cm.getNetworkInfo(mobile).isConnected() ||
cm.getNetworkInfo(wifi).isConnected()){
asyncTask = new TweetAsyncTask();
asyncTask.execute();
} else {
Toast.makeText(getActivity(),
"Sem conexão com a internet",
Toast.LENGTH_SHORT).show();
}
}
private void configuraAdapter(List<Tweet> tweets){
TweetAdapter adapter =
new TweetAdapter(getActivity(), tweets);
setListAdapter(adapter);
}
class TweetAsyncTask extends
AsyncTask<Void, Void, BuscaTwitter> {
@Override
protected void onPreExecute() {
super.onPreExecute();
showDialog();
}
@Override
protected BuscaTwitter doInBackground(
Void... params) {
String url =
"http://search.twitter.com/"+
"search.json?q=CPRecife";
try {
InputStream is = new URL(url).openStream();
Gson gson = new Gson();
BuscaTwitter resultadoBusca = gson.fromJson(
new InputStreamReader(is),
BuscaTwitter.class);
return resultadoBusca;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(BuscaTwitter result) {
super.onPostExecute(result);
if (result != null &&
result.getResults() != null) {
tweets = result.getResults();
configuraAdapter(tweets);
} else {
Toast.makeText(getActivity(),
"Falha ao carregar tweets",
Toast.LENGTH_SHORT).show();
}
dialog.dismiss();
}
}
}
Solução 2
Uma solução para dar um feedback para o usuário é a ProgressDialog. Ele será exibido no método
onPreExecute, e retirado da tela no método
onPostExecute. Esses métodos são chamados, como o próprio nome diz, antes e depois do download das informações. Se você não conhece a AsyncTask, da uma olhada
aqui.
Solução 3
Para verificar se existe conexão com a internet, estamos utilizando a classe ConnectivityManager. Essa checagem está sendo feita no método
iniciarDownload. Para utilizar essa classe é necessário adicionar a permissão ACCESS_NETWORK_STATE. Mais informações, é só olhar esse post
aqui.
Solução 4
Essa é a mais simples. No método que está baixando o JSON (
doInBackground) temos um try/catch, para que caso ocorra algum problema, a exceção seja capturada. Nesse caso o método retornará null. Assim, se no método
onPostExecute, o parâmetro
result vier nulo é porque houve algum problema. E nesse caso, mostrar uma mensagem pro usuário.
Solução 5
Essa é a parte mais complexa do post. Ao girar o aparelho, (por padrão) o Android destrói e recria a Activity que está sendo exibida. Para tratar esse comportamento, temos 3 opções conforme falei nesse post
aqui. As duas primeiras (fixar uma orientação e evitar que a Activity seja recriada) não são boas opções de design de software, pois elas impedem que criemos layouts diferentes para as duas orientações, e também impedem que ao mudarmos de idioma, a Activity recarregue os textos do idioma corrente.
Sendo assim, a API de Fragmentos nos dá a opção de manter seu estado mesmo que a activity seja destruída. Fazemo isso com o método
setRetainInstance do Fragment. No nosso fragmento temos três atributos:
- a AsyncTask que ferá o download das informações;
- uma lista com os Tweets que já foram baixados, e vai servir para que não tenhamos que baixar a informação novamente quando girarmos o aparelho;
- e um ProgressDialog que falamos na solução 2.
No método
onActivityCreate verificamos se a AsyncTask é igual a nulo, neste caso iniciamos o download das informações. Caso contrário, verificamos se a AsyncTask ainda está executando, em caso positivo, apenas exibimos o ProgressDialog.
Ao clicar no botão refresh, não podemos solicitar que a AsyncTask execute novamente, pois semelhante a classe Thread, não podemos reusá-la. Nesse caso, precisamos criar uma nova.
Para finalizar, se vocês observarem, estou utilizando a classe Gson para ler o JSON retornado pelo Twitter. Essa classe faz parte da biblioteca GSON, e faz o parse automático de um JSON para um objeto Java, desde que eles tenham a mestra estrutura. No nosso exemplo, a estrutura é representada pelas classes BuscaTwitter e Tweet. Para baixar a biblioteca do GSON é só clicar
aqui. Descompacte o arquivo e coloque o JAR na pasta libs do projeto.
O arquivo de layout do fragment é mostrado abaixo:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#cccccc" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:text="#CPRecife"
android:textAppearance="?android:attr/textAppearanceLarge" />
<ImageButton
android:id="@+id/btnRefresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/stat_notify_sync" />
</LinearLayout>
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Pronto! Agora você pode executar a aplicação e girar o aparelho a vontade que tudo deve funcionar bem :)
É isso pessoal. Qualquer dúvida, deixem seus comentários.
4br4ç05,
nglauber