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

Um comentário:

Deivison Francisco disse...

Outra forma de fazer seria deixar menos responsabilidade para o método onCreateView, deixando da seguinte forma:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.activity_dialog_result, container);
}

E utilizar o onViewCreated para tratar os elementos desejados :)

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
textView = (TextView) view.findViewById(R.id.textViewCalculateFinal);
closeDialog = (Button) view.findViewById(R.id.closeDialog);

String title = getArguments().getString(TITLE);
String mensage = getArguments().getString(MENSAGE);

getDialog().setTitle(title);
textView.setText(mensage);

closeDialog.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
dismiss();
}
});
}