quarta-feira, 1 de maio de 2013

AsyncTaskLoader

Olá povo,

Uma das coisas mais comuns em aplicações móveis é o acesso a servidores web através do protocolo HTTP. O Android obviamente nos permite esse tipo de comunicação de várias formas. Mas uma forma interessante foi introduzida na versão 3 da plataforma: os Loaders. Esse tipo de abordagem permite isolar a Activity da classe que está fazendo o download dos dados.
Um exemplo típico dessa abordagem é a utilização da classe AsyncTask - que eu sempre usei, e ainda uso - jutamente com um ProgressDialog. Normalmente o ProgressDialog era um atributo da AsyncTask  e no onPreExecute ele era exibido, sendo ocultado no onPostExecute. O problema dessa abordagem é que ao rotacionar a tela do aparelho, a Activity é recriada. Dessa forma, quando a AsyncTask terminava seu trabalho, a instância do Dialog armazenada ficava apontando para a instância da Activity que o tinha criado, gerando uma exceção em tempo de execução.

Com a chegada dos Fragments, é possível reter a instância do mesmo e ter um controle melhor sobre a rotação da tela (e outras mudanças de configuração). Mas a abordagem que eu achei interessante é a utilização da classe AsyncTaskLoader em conjunto com o LoadManager e a interface LoaderCallbacks. Vejamos o exemplo abaixo:
public class MainActivity 
  extends FragmentActivity 
  implements LoaderCallbacks<String> {

  ProgressDialog dialog;
  String json;
 
  private static final int MEU_LOADER = 0;

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

    // Recuperando o estado da Activity
    if (savedInstanceState != null) {
      json = savedInstanceState.getString("valores");
      atualizaTela();
    }

    // Essa classe gerencia todos os Loaders
    LoaderManager lm = getSupportLoaderManager();

    // Se o JSON ainda não foi baixado
    if (json == null) {
      // Exibe o dialog
      dialog = ProgressDialog.show(
        this, "Aguarde...", "Baixando tweets");

      // E inicializa o Loader (se ele já foi 
      // inicializado, ele apenas continuará)
      lm.initLoader(MEU_LOADER, null, this);
    }
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    // Se estiver exibindo o progress, remova-o 
    if (dialog != null && dialog.isShowing()) 
      dialog.dismiss();
  } 

  @Override
  protected void onSaveInstanceState(Bundle outState){
    super.onSaveInstanceState(outState);
    // Salvando o estado da Activity
    outState.putString("valores", json);
  }

  // Métodos da interface LoaderCallbacks -----------
  @Override
  public Loader<String> onCreateLoader(
    int id, Bundle args) {
    // Instancia e retorna a AsyncTaskLoader
    return new MinhaAsyncTask(this);
  }

  @Override
  public void onLoadFinished(Loader<String> loader, 
    String dados) {
    // Chamado quando o Loader termina seu trabalho
    // Aqui, atribuímos o resultado ao 
    // atributo json e ocultamos o dialog
    json = dados;
    atualizaTela();
    dialog.dismiss();
  }

  @Override
  public void onLoaderReset(Loader<String> loader) {
     // Chamado se o Loader for resetado
     Log.d("NGVL", "onLoaderReset");
  }

  // AsyncTaskLoader
  public static class MinhaAsyncTask 
    extends AsyncTaskLoader<String> {

    // O JSON baixado será armazenado aqui para ser 
    // repassado para Activity
    String data;

    public MinhaAsyncTask(Context context) {
      super(context);
    }

    @Override
    protected void onStartLoading() {
      // Se não baixou os dados, faça agora!
      if (data == null) {
        forceLoad();

      // Se já baixou, apenas entregue o resultado 
      } else {
        deliverResult(data);
      }
    }

    @Override
    public String loadInBackground() {
      // Nesse método será feito o download do JSON
      return downloadJSON();
    }
  }
}
O código acima está todo comentado. Então vou focar nos detalhes que devem ser observados.
O primeiro ponto é utilizar as classes do pacote android.support.v4.* pois o conceito de Loaders só surgiu no Honeycomb, então utilizei aqui a API de compatibilidade para que esse código execute sem problemas nas versões anteriores ao Android 3. O segundo ponto interessante é que, com essa abordagem o download não é reiniciado quando a Activity é recriada.
Outro detalhe é que, caso você queira chamar o loader novamente, você deve usar o método restartLoader e não o initLoader. E se quiser passar parâmetros para o Loader, pode usar um objeto Bundle e passá-lo como o segundo argumento desses métodos.
getSupportLoaderManager().restartLoader(
  MEU_LOADER, null, this);
Os métodos downloadJSON() e atualizaTela() devem ser implementados por você :) para baixar o JSON desejado e atualizar a tela da maneira apropriada.
Abaixo eu coloquei um exemplo desses dois métodos que baixa o JSON de uma busca no Twitter e exibe os textos dos Tweets em uma ListView.
private void atualizaLista() {
  if (json == null) return;
  try {
    JSONObject jsonObj = new JSONObject(json);
    JSONArray jsonResults = 
      jsonObj.getJSONArray("results");
   
    List<String> texts = new ArrayList<String>();
    for (int i = 0; i < jsonResults.length(); i++) {
      JSONObject result = jsonResults.getJSONObject(i);
      texts.add(result.getString("text"));
    }
   
    ListView list = (ListView)
      findViewById(R.id.ListView1);

    list.setAdapter(new ArrayAdapter<String>(this, 
      android.R.layout.simple_list_item_1, texts));
   
  } catch (JSONException e) {
    e.printStackTrace();
  }
}
 
private static String downloadJSON() {
  try {
    URL url = new URL(
      "http://search.twitter.com/search.json?q=jctransito");
    HttpURLConnection conexao = (HttpURLConnection)
      url.openConnection();
    conexao.connect();
   
    InputStream is = conexao.getInputStream();
    BufferedReader reader = new BufferedReader(
      new InputStreamReader(is));

    String s = reader.readLine();
    return s;
   
  } catch (Exception e) {
    e.printStackTrace();
  }
  return null;
}
Não esqueça de adicionar a permissão de INTERNET no AndroidManifest.xml.

Com abordagem acima, mesmo ao girar o aparelho, o download continuará, e caso não tenha terminado quando a Activity carregar, o ProgressDialog será exibido novamente.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Referências:
http://developer.android.com/reference/android/app/LoaderManager.html
http://developer.android.com/reference/android/app/LoaderManager.LoaderCallbacks.html
http://developer.android.com/reference/android/content/AsyncTaskLoader.html

5 comentários:

Unknown disse...

Como parar a execução da "AsyncTaskLoader" pressionando o botão voltar (KEYCODE_BACK KeyDown), pois gostaria de deixar o usuário decidir se ele quer ou não aguardar esse processo ?

Mas foi muito Interessante o seu post e resolve meus problemas ao realizar download de dado. Parabéns!

Unknown disse...

Saberia me informar, se dentro de um processo lento de conexão com o servidor
aguardando a resolução de um SQL (Tipo 1 minuto).

O AsyncTaskLoader resolveria essa situação ?

Pois venho a tempo tentando uma Thread Ilimitada para executar procedimentos em Backgound.

Até então o AsyncTaskLoader me pareceu ser a melhor solução mas ele não aguenta 10 segundos e estoura o ANR.

Se puder me ajudar, ficarei agradecido, pois estou ficando muito louco com isso.

Obrigado !

Unknown disse...

Muito útil a dica do "AsyncTaskLoader". Certeza que vou usar, Inclusive, em versões do sdk maior que 9. O Android nem permite que você espere uma resposta de um get sem estar em uma thread. Levanta a exceção:
“Network OnMainThreadExveption”

Para permitir que a aplicação espere a resposta tem que da permissão com “ThreadPolicy.”. Claro que não é uma boa pratica.

StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();

StrictMode.setThreadPolicy(policy);

Unknown disse...

Olá, primeiramente parabéns pelo blog , é o melhor conteúdo em português que eu achei até agora. Gostaria de questioná-lo a respeito da leitura de json , estou lendo um json um pouco grande de mais ou menos 6000 mil registros de um bd mysql, e a vezes tenho tido problema com uma excessão de memória, existe alguma técnica específica para evitar esse problema, qual a melhor maneira de se fazer isso , desde já muito obrigado.

Nelson Glauber disse...

Oi Marcelo,

Obrigado pelos elogios :)

A técnica mais adotada é a paginação. Na primeira solicitação, seu JSON retorna 100 registros (por exemplo), e além dessa lista, um token para a próxima página.
Da segunda requisição em diante, você pegará o token da requisição anterior e fará uma nova requisição.

É assim que Google, Fabebook, Twitter fazem para retornar seus dados...

Uma outra sugestão é zipar seu JSON no servidor e dezipar no cliente. Diminuindo o tempo e a quantidade de dados lidos da rede.

4br4ç05,
nglauber