Mostrando postagens com marcador Orientação. Mostrar todas as postagens
Mostrando postagens com marcador Orientação. Mostrar todas as postagens

sexta-feira, 10 de agosto de 2012

Cuide da sua AsyncTask

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

quarta-feira, 26 de outubro de 2011

Android: Tratando mudança de orientação

Olá povo,

Chegaram 4 novos estagiários aqui no projeto e coube "a minha pessoa" orientá-los. Durante a aula, o assunto enveredou para tratamento de mudança de orientação do aparelho (portrait e landscape | retrato e paisagem).
Por padrão, ao girar o telefone o método onCreate é chamado novamente e com isso, dados que foram carregados dinamicamente (como itens de um ArrayList que estão preenchendo um ListView) são perdidos.

Vejamos o exemplo abaixo:

Arquivo de layout.
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:orientation="vertical"
 android:background="#0000FF">

 <TextView
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:text="Digite o nome" />

 <EditText
   android:id="@+id/editText1"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"/>

 <Button
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:onClick="meuBotaoClick"
   android:text="Adicionar" />

 <ListView
   android:id="@+id/listView1"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" />
</LinearLayout>

Classe da Activity
public class TelaPrincipalActivity
 extends Activity {

 EditText edt;
 ArrayList<String> nomes;
 ArrayAdapter<String> adapter;

 @Override
 public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.main);

   edt = (EditText) findViewById(R.id.editText1);

   ListView listView = (ListView) 
     findViewById(R.id.listView1);
   adapter = new ArrayAdapter<String>(this,
     android.R.layout.simple_list_item_1, nomes);

   listView.setAdapter(adapter);
 }

 public void meuBotaoClick(View v) {
   nomes.add(edt.getText().toString());
   edt.setText("");
   adapter.notifyDataSetChanged();
 }
}

No código acima, ao clicar no botão, o texto digitado no EditText é inserido na lista, e em seguida o adapter é atualizado através do método notifyDataSetChanged.
O exemplo é bem simples, mas se você girarmos o aparelho os dados que estão sendo exibidos são perdidos, uma vez que a lista inicia vazia e (como já falei) o onCreate é chamado novamente.

Para isso não acontecer você tem 3 alternativas:

1)Forçar uma orientação
Na declaração da sua Activity, no AndroidManifest.xml, você pode usar a propriedade android:screenOrientation para manter sua aplicação em uma orientação específica (portrait/landscape).
<activity
 android:name="TelaPrincipalActivity"
 android:screenOrientation="portrait" />


2)Avisar o Android para não chamar o onCreate
Essa é a melhor opção quando você quer utilizar a tela nas duas orientações e impedir que o Android chame o onCreate cada vez que você girar o aparelho. Para isso basta dizer que ao mudar a configuração (configChanges) de orientação, ele não recrie a Activity.
<activity
 android:name="TelaPrincipalActivity"
 android:configChanges="orientation|keyboardHidden|screenSize"/>


3) Salvar o estado da Activity
A opção número dois é perfeita quando você quer usar o mesmo layout nas duas orientações. Mas, e se eu quiser usar layouts diferentes? Neste caso você dever permitir que a Activity seja recriada. Entretanto, devemos salvar seu estado com o método onSaveInstanceState e no onCreate recuperar esse estado.

No exemplo que fiz os "estags", criei a pasta "layout-land" com o mesmo layout definido acima, só mudando o background (apenas para notarmos os diferentes layouts). Depois implementamos o método onSaveInstanceState para salvar os itens da lista.
@Override
protected void onSaveInstanceState(Bundle outState) {
 super.onSaveInstanceState(outState);
 outState.putStringArrayList("nomes", nomes);
}

Se vocês notaram, o método onSaveInstanceState usa um objeto da classe Bundle para salvar o estado. Esse estado é passado para o método onCreate da Activity como parâmetro, que utilizaremos para restaurar o seu estado.
@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // Código já apresentado
 if (savedInstanceState != null) {
   nomes = savedInstanceState.
    getStringArrayList("nomes");

 } else {
   nomes = new ArrayList<String>();
 }
}


Com isso podemos ter, para a mesma tela, um layout diferente para cada orientação, desde que tenhamos os mesmos componentes (e com os mesmos ids).

Ficou com dúvida? Deixe seu comentário.

4br4ç05,
nglauber

sábado, 6 de agosto de 2011

Artigo "Sensores no Android"

Olá povo,

A revista Java Magazine edição 94, trás uma matéria sobre utilização de sensores na plataforma Android. Ela foi escrita por mim e pelos meus colegas Edilson Mendes(Ronaldo!) e Artur Botelho (o doido :)

Este artigo demonstra como obter informações dos sensores dos aparelhos Android através da Sensor API. Com ela podemos capturar informações como aceleração e orientação do aparelho. Estes e outros sensores podem servir tanto como mecanismo de iteração com o usuário, quanto de canal de comunicação para obter informações sobre o dispositivo e o ambiente onde ele se encontra. O sensor de aceleração, também conhecido como acelerômetro, por exemplo, é amplamente utilizado em jogos disponíveis no Android Market.

Espero que vocês gostem.

4br4ç05,
nglauber

quinta-feira, 2 de dezembro de 2010

Android: Dicas de programação da semana

Olá povo,

No novo projeto em que estou envolvido estamos desenvolvendo várias aplicações. Como temos várias pessoas envolvidas, algumas dúvidas se tornam recorrentes, então resolvi postá-las.

Pergunta 1: Quando eu giro o telefone o método onCreate da Activity é chamado novamente. Tem como evitar isso?

Resposta: Sim, basta adicionar a propriedade android:configChanges na tag <activity> do seu AndroidManifest.xml. Isso evitará que a atividade seja recriada.

<activity android:name="MinhaActivity"
android:configChanges="keyboardHidden|orientation"/>

No caso acima, se o telefone mudar de orientação ou se o teclado físico (se houver) for aberto, a atividade não será recriada.

Pergunta 2: Tem como deixar uma atividade apenas em landscape ou portrait?

Resposta: Sim, basta utilizar a propriedade android:screenOrientation na tag <activity> do seu AndroidManifest.xml.

<activity name=".MinhaActivity"
android:screenOrientation="landscape"/>


Pergunta 3: Como permitir apenas letras na caixa de texto (EditText)?

Resposta: Não existe uma propriedade para fazer isso. Se quisermos colocar para aceitar apenas números podemos utilizar a propriedade android:inputType="number". Ou quando queremos deixar todas as letras em maiúsculo podemos usar o valor textCapCharacters. Mas para não aceitar nem números nem símbolos só com a boa e velha expressão regular.

Pattern pattern = Pattern.compile(
"^[A-ZÁÀÄÃÂÆÅÉÈËÊÍÌÏÎÓÒÖÕÔØŒÚÙÜÛÝÿYÑÇ ]+$");

Matcher matcher = pattern.matcher(
meuEditText.getText().toString().toUpperCase());

if (!matcher.matches()){
Log.e("ERRO",
"Esse campo só aceita letras e espaço em branco!");
}


Pergunta 4: Como fazer uma lista múltipla que eu possa selecionar vários itens?

Resposta: Devo confessar que esse problema me encheu muito o saco. Não para criar a lista, mas sim para obter os itens selecionados. Pra informar que sua ListView permite seleção múltipla use o método setChoiceMode(int).

lstView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

Feito isso, sua lista já permitirá seleção múltipla. Agora como fazer para obter os itens selecionados? Bem, a classe ListView tem dois métodos que você pode obter arrays com as posições e os ids dos itens selecionados: getCheckItemPositions() e getCheckItemIds(). Infelizmente não tive uma boa experiência com eles. Ao marcar e desmarcar itens, aconteciam momentos em que os itens ficavam inconsistentes, ou seja, ele informava que itens estavam marcados, quando na verdade não estavam. Sendo assim, preferi fazer da minha forma:

private int getCheckedCount(){
lstView.getCheckItemIds();

int count = 0;
int listSize = lstView.getAdapter().getCount();
for (int i = 0; i < listSize; i++) {
if (lstView.isItemChecked(i)){
count++;
}
}
return count;
}

// Para marcar ou desmarcar um item específico,
// podemos usar:
lstView.setItemChecked(posicao, false);


Pergunta 5. Como carregar uma imagem da aplicação baseada no nome do arquivo?

Resposta: Se esse recurso estiver na pasta res/drawable você pode fazer via reflection. Eu já fiz um post sobre isso, que vocês podem conferir aqui. Mas se quiser usar de maneira descente :) podem usar a pasta assets do seu projeto. Coloque as imagens que você deseja carregar dinamicamente nesse diretório e faça como abaixo:

try {
InputStream is = getAssets().open("imagem.png");
Bitmap bmp = BitmapFactory.decodeStream(is);
ImageView img = (ImageView)
findViewById(R.id.ImageView01);

img.setImageBitmap(bmp);

} catch (IOException e) {
e.printStackTrace();
}


Pergunta 6. Como salvar uma informação simples como o recorde do jogo?

Resposta: Para persistir informações simples, o Android disponibiliza a classe SharedPreferences onde você pode salvar pequenas configurações. Vejam abaixo como ler/escrever uma preferência através de dois métodos de exemplo que salvam e recuperam o recorde de pontos de um jogo.

public int getRecord(){
SharedPreferences prefs =
context.getSharedPreferences(
"configs", Context.MODE_PRIVATE);

return prefs.getInt("record", 0);
}

public void setRecord(int record){
SharedPreferences prefs =
context.getSharedPreferences(
"configs", Context.MODE_PRIVATE);

SharedPreferences.Editor editor = prefs.edit();
editor.putInt("record", record);
editor.commit();
}


Pergunta 7. Como ler um XML do diretório res/xml?

Resposta: Quem é meu aluno já conhece essa frase que eu uso sempre nas aulas: Existem mil maneiras de se fazer Neston :) Uma das maneiras de ler um XML é usando o XMLResourceParser, segue um exemplo bem simples do seu uso. Clique aqui pra ver o post original.


private String getEventsFromAnXML(Context c)
throws XmlPullParserException, IOException {

StringBuffer stringBuffer = new StringBuffer();
Resources res = c.getResources();
XmlResourceParser xpp = res.getXml(R.xml.meu_xml);
xpp.next();

int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT){
if(eventType == XmlPullParser.START_DOCUMENT) {
stringBuffer.append("--- Start XML ---");

} else if(eventType == XmlPullParser.START_TAG){
stringBuffer.append("\nSTART_TAG: "+
xpp.getName());

} else if(eventType == XmlPullParser.END_TAG){
stringBuffer.append("\nEND_TAG: "+
xpp.getName());

} else if(eventType == XmlPullParser.TEXT) {
stringBuffer.append("\nTEXT: "+xpp.getText());
}
eventType = xpp.next();
}
stringBuffer.append("\n--- End XML ---");
return stringBuffer.toString();
}

De acordo com a evolução do projeto vou colocando mais dicas.

4br4ç05,
nglauber