Mostrando postagens com marcador Fragments. Mostrar todas as postagens
Mostrando postagens com marcador Fragments. Mostrar todas as postagens

sexta-feira, 11 de outubro de 2013

Adeus AlertDialog! Bem-vindo DialogFragment

Olá povo,

Até a versão 2.3 do Android (API Level 10), a forma padrão de exibir mensagens para os usuários era através da classes AlertDialog, mas partir da versão 3 do Android (Honeycomb API Level 11) uma nova abordagem foi adotada, a utilização da classe DialogFragment.
Apesar de ter sido lançada na versão 3, a API de compatibilidade do Android nos permite usar esse recurso em versões anteriores. Umas das vantagens dessa abordagem é o maior controle sobre o ciclo de vida do Dialog, uma vez que ele nada mais é do que um Fragment.
Apenas para relembrar, até o 2.3 fazíamos isso:

int x = 0;
 
@Override
protected Dialog onCreateDialog(int id) {
  AlertDialog dialog = new AlertDialog.Builder(this)
    .setTitle("Título")
    .setMessage("Deseja exibir "+ x++)
    .setPositiveButton("Sim", null) // Listener aqui
    .setNegativeButton("Não", null) // e aqui
    .create();
  return dialog;
}
 
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
  ((AlertDialog)dialog).setMessage("Mensagem "+ x++);
  super.onPrepareDialog(id, dialog);
}
 
public void abrirAlert(View v){
  showDialog(0);
}
A abordagem acima era a recomendada pois, se abríssimos um dialog e girássemos a tela, o mesmo era perdido. No método onCreateDialog, é criado o dialog (dããã), mas se precisássemos alterar o texto mudando apenas um detalhe (no exemplo acima, o valor de x) deveríamos usar o método onPrepareDialog conforme acima. Todos os dialogs da Acvity deviam ser criados no onCreateDialog e cada um tem o seu ID que é recebido por parâmetro. Para exibir o dito-cujo, era só chamar showDialog(ID).

A classe DialogFragment envolve (wraps) um AlertDialog, ou pode ter qualquer view. Podemos dizer que seria como um "Fragment Modal". No exemplo abaixo, temos um exemplo de um DialogFragment que recebe um título, uma mensagem e os (de 1 a 3) botões.

import android.app.AlertDialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;

public class SimpleDialog extends DialogFragment 
  implements OnClickListener {
 
  private static final 
    String EXTRA_ID      = "id";
  private static final 
    String EXTRA_MESSAGE = "message";
  private static final 
    String EXTRA_TITLE   = "title";
  private static final 
    String EXTRA_BUTTONS = "buttons";
  private static final 
    String DIALOG_TAG    = "SimpleDialog";
 
  private int dialogId;
 
  public static SimpleDialog newDialog(int id, 
    String title, String message, int[] buttonTexts){
    // Usando o Bundle para salvar o estado
    Bundle bundle  = new Bundle();
    bundle.putInt(EXTRA_ID, id);
    bundle.putString(EXTRA_TITLE, title); 
    bundle.putString(EXTRA_MESSAGE, message);
    bundle.putIntArray(EXTRA_BUTTONS, buttonTexts);
  
    SimpleDialog dialog = new SimpleDialog();
    dialog.setArguments(bundle);
    return dialog; 
  }
 
  @Override
  public Dialog onCreateDialog(
    Bundle savedInstanceState) {

    String title = getArguments()
      .getString(EXTRA_TITLE);
    String message = getArguments()
      .getString(EXTRA_MESSAGE);
    int[] buttons = getArguments()
      .getIntArray(EXTRA_BUTTONS);
     
    AlertDialog.Builder alertDialogBuilder = 
      new AlertDialog.Builder(getActivity());
    alertDialogBuilder.setTitle(title);
    alertDialogBuilder.setMessage(message);
        
    switch (buttons.length) {
      case 3:
        alertDialogBuilder.setNeutralButton(
          buttons[2], this);

      case 2:
        alertDialogBuilder.setNegativeButton(
          buttons[1], this);
   
      case 1:
        alertDialogBuilder.setPositiveButton(
          buttons[0], this);
    }    
    return alertDialogBuilder.create();
  }
    
  @Override
  public void onClick(
    DialogInterface dialog, int which) {
    // Sua Activity deve implementar essa interface
    ((FragmentDialogInterface)getActivity())
      .onClick(dialogId, which);
  }

  public void openDialog(
    FragmentManager supportFragmentManager) {

    if (supportFragmentManager.findFragmentByTag(
      DIALOG_TAG) == null){

      show(supportFragmentManager, DIALOG_TAG);
    }  
  }
  // Interface que erá chamada ao clicar no bot"ao
  public interface FragmentDialogInterface {
    void onClick(int id, int which);
  }
}
Podemos notar no código acima que estamos herdando da classe DialogFragment. Criei um método estático para inicializar o Dialog, lembrando que essa é uma boa prática uma vez que sempre devemos ter o construtor padrão e o Bundle armazenará o estado do Fragment. No método onCreateDialog, inicializamos o AlertDialog com os dados passados no "método construtor".
Nossa Activity deve implementar essa interface, pois não podemos manter a referência dela, uma vez que ela pode ter sido destruída ao girar o aparelho. Para abrir o dialog, verificamos se ele já foi adicionado ao FragmentManager, caso contrário, exibimos.
Pra exibir o dialog, basta fazer conforme abaixo:
public void abrirSimpleDialog(View v) {
  SimpleDialog dialog = SimpleDialog.newDialog(
    0,              // Id do dialog
    "Alerta",       // título
    "Mensagem",     // mensagem
    new int[]{      // texto dos botões
      android.R.string.ok, 
      android.R.string.cancel });
    dialog.openDialog(getSupportFragmentManager());
  }

  @Override
  public void onClick(int id, int which) {
    Toast.makeText(MainActivity.this, 
      "Botão clicado "+ which, Toast.LENGTH_SHORT)
        .show();
  }
Agora se quisermos um Dialog customizado, para por exemplo, receber input do usuário, podemos criar um arquivo de layout e associá-lo ao DialogFragment.
 
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/edtName"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_gravity="center"
  android:orientation="vertical"
  android:padding="16dp" >

  <TextView
    android:id="@+id/lbl_your_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Your name" />

  <EditText
    android:id="@+id/txt_your_name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:imeOptions="actionDone"
    android:inputType="text" />

</LinearLayout>
O Layout é simple e contém apenas um TextView e um EditText. Vamos ver como ficará o DialogFragment que usaria esse layout.
public class EditNameDialog 
  extends DialogFragment 
  implements OnEditorActionListener {

  private static final 
    String DIALOG_TAG = "editDialog";
  private static final 
    String EXTRA_INPUT_TEXT = "message";
  private static final 
    String EXTRA_TITLE = "inputText";

  private EditText mEditText;

  // Sua Activity deve implementar essa Interface
  public interface EditNameDialogListener {
    void onFinishEditDialog(String inputText);
  }

  public static EditNameDialog newInstance(
    String title, String inputText) {

    Bundle bundle = new Bundle();
    bundle.putString(EXTRA_TITLE, title);
    bundle.putString(EXTRA_INPUT_TEXT, inputText);

    EditNameDialog dialog = new EditNameDialog();
    dialog.setArguments(bundle);
    return dialog;
  }

  @Override
  public View onCreateView(LayoutInflater inflater, 
    ViewGroup container, Bundle savedInstanceState){

    String title = getArguments()
      .getString(EXTRA_TITLE);
    String inputText = getArguments()
      .getString(EXTRA_INPUT_TEXT);

    View view = inflater.inflate(
      R.layout.layout_dialog, container);

    TextView txtView = (TextView)
      view.findViewById(R.id.lbl_your_name);
    txtView.setText(inputText);
    // Exibe o teclado virtual ao exibir o Dialog
    getDialog().getWindow().setSoftInputMode(
      WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);

    getDialog().setTitle(title);

    mEditText = (EditText)
      view.findViewById(R.id.txt_your_name);
    mEditText.requestFocus();
    // Listener para quando clicarmos 
    // em 'Done' no teclado
    mEditText.setOnEditorActionListener(this);

    return view;
  }

  @Override
  public boolean onEditorAction(TextView v, 
    int actionId, KeyEvent event) {
    // Se clicou em 'Done'
    if (EditorInfo.IME_ACTION_DONE == actionId) {
      // Notifique a Activity
      EditNameDialogListener activity = 
        (EditNameDialogListener) getActivity();
      activity.onFinishEditDialog(
        mEditText.getText().toString());
      // Feche o dialog
      dismiss();
      return true;
    }
    return false;
  }

  public void openDialog(FragmentManager fm) {
    if (fm.findFragmentByTag(DIALOG_TAG) == null) {
      show(fm, DIALOG_TAG);
    }
  }
}
Para exibir e tratar esse dialog usamos o código abaixo:
public void abrirEditDialog(View v){
  EditNameDialog editNameDialog = 
    EditNameDialog.newInstance(
      "Informação", "Digite seu nome");

  editNameDialog.openDialog(
    getSupportFragmentManager());
}
 
@Override
public void onFinishEditDialog(
  String inputText) {

  Toast.makeText(this, "Olá, " + inputText, 
    Toast.LENGTH_SHORT).show();  
}
O resultado ficará conforme a figura.


[EDITADO em (11/10/2013 às 11:20)]
Como você devem ter notado, em ambos os exemplo estamos usando a Activity para tratar o retorno do Dialog. Mas e se quisermos chamar o Dialog a partir de um Fragment?
Nesse caso podemos utilizar a propriedade Target Fragment. Ela funciona como o startActivityForResult, só que para Fragments.
// A partir de um Fragment
public void abrirSimpleDialog() {
  SimpleDialog dialog = SimpleDialog.newDialog(
    0, // Id do dialog
    "Alerta", // título
    "Mensagem", // mensagem
    new int[] { // texto dos botões
      android.R.string.ok, 
      android.R.string.cancel });
  // Segredo do sucesso! :)
  // 1 = RequestCode
  dialog.setTargetFragment(this, 1); 
  dialog.openDialog(
    getActivity().getSupportFragmentManager());
}

@Override
public void onActivityResult(int requestCode, 
  int resultCode, Intent data) {

  super.onActivityResult(
    requestCode, resultCode, data);

  int which = data.getIntExtra("which", -1);
  // Tratar dialog  
}
Com essa alteração, a interface FragmentDialogInterface pode ser eliminada (uma vez que a Activity não precisa mais implementá-la), e o código do onClick da classe SimpleDialog, ficaria dessa forma:
@Override
public void onClick(DialogInterface dialog, int which) {
  Intent it = new Intent();
  it.putExtra("which", which);
  // Chamando o onActivityResult do targetFragment
  getTargetFragment().onActivityResult(
    getTargetRequestCode(), Activity.RESULT_OK, it);
}

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

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