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.
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