DialogFragment callback on orientation change
Asked Answered
V

5

19

I'm migrating my dialogs, currently using Activity.showDialog(DIALOG_ID);, to use the DialogFragment system as discussed in the android reference.

There's a question that arose during my development when using callbacks to deliver some event back to the activity/fragment that opened the dialog:

Here's some example code of a simple dialog:

public class DialogTest extends DialogFragment {

public interface DialogTestListener {
    public void onDialogPositiveClick(DialogFragment dialog);
}

// Use this instance of the interface to deliver action events
static DialogTestListener mListener;

public static DialogTest newInstance(Activity activity, int titleId, int messageId) {
    udateListener(activity);
    DialogTest frag = new DialogTest();
    Bundle args = new Bundle();
    args.putInt("titleId", titleId);
    args.putInt("messageId", messageId);
    frag.setArguments(args);
    return frag;
}

public static void udateListener(Activity activity) {
    try {
        // Instantiate the NoticeDialogListener so we can send events with it
        mListener = (DialogTestListener) activity;
    } catch (ClassCastException e) {
        // The activity doesn't implement the interface, throw exception
        throw new ClassCastException(activity.toString() + " must implement DialogTestListener");
    }
}


@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    int titleId = getArguments().getInt("titleId");
    int messageId = getArguments().getInt("messageId");

    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // dialog title
    builder.setTitle(titleId);
    // dialog message
    builder.setMessage(messageId);

    // dialog negative button
    builder.setNegativeButton("No", new OnClickListener() {
               public void onClick(DialogInterface dialog, int id) {}});
    // dialog positive button
    builder.setPositiveButton("Yes", new OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            mListener.onDialogPositiveClick(DialogTest.this);
        }});

    // create the Dialog object and return it
    return builder.create();
}}

And here's some activity code calling it:

public class SomeActivity extends FragmentActivity implements DialogTestListener {
private EditText mUserName;
@Override
public void onCreate(Bundle savedInstanceState) {
    // setup ui
    super.onCreate(savedInstanceState);
    setContentView(R.layout.ui_user_edit);
    // name input
    mUserName = (EditText) findViewById(R.id.userEdit_editTextName);
}

@Override
public void onDialogPositiveClick(DialogFragment dialog) {
    Log.d(TAG, this.toString());
    mUserName.setText(mUserName.getText() + "1");
}

private void showDialog() {
    DialogTest test = DialogTest.newInstance(SomeActivity.this, R.string.someTitleText, R.string.someMessageText);
    test.show(getSupportFragmentManager(), "testDialog");
}}

The code is pretty much what you see the reference. Problem is, that once you do a orientation change, when a dialog is shown, it stops working as expected --> Due to the activity lifecycle, both, the activity and the dialog are rebuild, and the dialog now does not have the proper reference to the new rebuilt activity.

I added the following code to my activitys onResume method:

    @Override
protected void onResume() {
    super.onResume();
    DialogTest.udateListener(this);
}

Doing this, I get the expected behavior, and the dialog sends events back to the new rebuilt activity when an orientation change occured.

My question is: What is the 'best practice' to handle the callbacks between the DialogFragment which was opened by a FragmentActivity during an orientation change?

Best regards

Vesuvian answered 10/10, 2012 at 14:50 Comment(1)
Sounds like you've fallen down one many Fragment pitfalls. I had the same problem as you and I was able to fix it whilst reading through this excellent article: code.hootsuite.com/orientation-changes-on-android .Overcloud
H
9

Yeah, this is a common trap I'm falling in all the time myself. First of all let me say that your solution of calling DialogTest.udateListener() in onResume() seems to be fully appropriate to me.

An alternative way would be to use a ResultReceiver which can be serialized as a Parcelable:

public class DialogTest extends DialogFragment {

public static DialogTest newInstance(ResultReceiver receiver, int titleId, int messageId) {
    DialogTest frag = new DialogTest();
    Bundle args = new Bundle();
    args.putParcelable("receiver", receiver);
    args.putInt("titleId", titleId);
    args.putInt("messageId", messageId);
    frag.setArguments(args);
    return frag;
}

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    int titleId = getArguments().getInt("titleId");
    int messageId = getArguments().getInt("messageId");
    ResultReceiver receiver = getArguments().getParcelable("receiver");

    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // dialog title
    builder.setTitle(titleId);
    // dialog message
    builder.setMessage(messageId);

    // dialog negative button
    builder.setNegativeButton("No", new OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            receiver.sendResult(Activity.RESULT_CANCEL, null);
        }});
    // dialog positive button
    builder.setPositiveButton("Yes", new OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            receiver.sendResult(Activity.RESULT_OK, null);
        }});

    // create the Dialog object and return it
    return builder.create();
}}

Then you can handle everything in the Receiver like this:

protected void onReceiveResult(int resultCode, Bundle resultData) {
    if (getActivity() != null){
        // Handle result
    }
}

Check out ResultReceiver doesn't survire to screen rotation for more details. So in the end you probably still need to rewire the ResultReceiver with your Activity. The only difference is that you decouple the Activity from the DialogFragment.

Homochromous answered 18/10, 2012 at 13:38 Comment(4)
Thx for your input! For now I'll use the onResume() solution, once I need some Activity<->DialogFragment decoupling I'll have a look at the ResultReceiver.Vesuvian
Check out Fragment#setRetainInstance(boolean) and read through the documentation. This is actually the solution we're really looking for. While the Activity is still being destroyed and recreated, the Fragment is being retained and reused. Therefore the callback DialogTestListener is still pointing to the correct object and you don't have to rewire the Fragments after configuration change.Zrike
The problem with this solution is that if you activate "Don't keep activities" open the dialog, then press HOME and open the App from the home launcher, the dialog will be recreated and you will get a BadParcelableException, I'm struggling on this.Platitudinous
Very clean solution; had not heard of ResultReceiver before.Argol
T
15

There is better solution instead of using static methods and variables because it would work only fro one instance of your dialog. It is better to store your callback as non static member

private DialogTestListener mListener;
public void setListener (DialogTestListener listener){
  mListener = listener;
}

Then you should show your dialog using TAG like this mDialogFragment.show(getSupportFragmentManager(), DIALOG_TAG);

And then in onResume method of your activity you can reset your listener

protected void onResume() {
   super.onResume();
   mDialogFragment = (CMFilterDialogFrg) getSupportFragmentManager().findFragmentByTag(DIALOG_TAG);
   if(mDialogFragment  != null){
       mDialogFragment.setListener(yourListener)
   }
}
Theurich answered 20/11, 2014 at 17:7 Comment(4)
Thanks - this method worked for me! First time for me using Dialog Fragments with a callback. I was wrestling with this for a while - thankfully I saw this post. I was nearly there myself but this post helped reach a solution +1 !Legendary
Jason Long gives the best solution if you call the dialog from an Activity, cuasodayleo gives the best solution if you call it from a Fragment, and this is the best solution if you need to face both scenarios.Johore
Thank you very much .. This is very easy to implement and straightforwardAsa
This won't survive orientation changes. getSupportFragmentManager().findFragmentByTag(DIALOG_TAG); will be nullLivi
H
9

Yeah, this is a common trap I'm falling in all the time myself. First of all let me say that your solution of calling DialogTest.udateListener() in onResume() seems to be fully appropriate to me.

An alternative way would be to use a ResultReceiver which can be serialized as a Parcelable:

public class DialogTest extends DialogFragment {

public static DialogTest newInstance(ResultReceiver receiver, int titleId, int messageId) {
    DialogTest frag = new DialogTest();
    Bundle args = new Bundle();
    args.putParcelable("receiver", receiver);
    args.putInt("titleId", titleId);
    args.putInt("messageId", messageId);
    frag.setArguments(args);
    return frag;
}

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    int titleId = getArguments().getInt("titleId");
    int messageId = getArguments().getInt("messageId");
    ResultReceiver receiver = getArguments().getParcelable("receiver");

    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // dialog title
    builder.setTitle(titleId);
    // dialog message
    builder.setMessage(messageId);

    // dialog negative button
    builder.setNegativeButton("No", new OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            receiver.sendResult(Activity.RESULT_CANCEL, null);
        }});
    // dialog positive button
    builder.setPositiveButton("Yes", new OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            receiver.sendResult(Activity.RESULT_OK, null);
        }});

    // create the Dialog object and return it
    return builder.create();
}}

Then you can handle everything in the Receiver like this:

protected void onReceiveResult(int resultCode, Bundle resultData) {
    if (getActivity() != null){
        // Handle result
    }
}

Check out ResultReceiver doesn't survire to screen rotation for more details. So in the end you probably still need to rewire the ResultReceiver with your Activity. The only difference is that you decouple the Activity from the DialogFragment.

Homochromous answered 18/10, 2012 at 13:38 Comment(4)
Thx for your input! For now I'll use the onResume() solution, once I need some Activity<->DialogFragment decoupling I'll have a look at the ResultReceiver.Vesuvian
Check out Fragment#setRetainInstance(boolean) and read through the documentation. This is actually the solution we're really looking for. While the Activity is still being destroyed and recreated, the Fragment is being retained and reused. Therefore the callback DialogTestListener is still pointing to the correct object and you don't have to rewire the Fragments after configuration change.Zrike
The problem with this solution is that if you activate "Don't keep activities" open the dialog, then press HOME and open the App from the home launcher, the dialog will be recreated and you will get a BadParcelableException, I'm struggling on this.Platitudinous
Very clean solution; had not heard of ResultReceiver before.Argol
O
4

While André's solution works, a better solution is to get the updated activity during onAttach() in your Fragment.

private DialogTestListener mListener;

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    mListener = (DialogTestListener) activity;
}

With this solution, you won't need to pass the Activity in newInstance() anymore. You just need to make sure the Activity owning your Fragment is a DialogTestListener. You also don't need to save the state like in the ResultReceiver solution.

Ostensorium answered 22/9, 2014 at 21:28 Comment(0)
E
1

First, call setTargetFragment from FragmentParent to start dialogFragment. In dialogFragment use getTargetFragment to callback fragment and return data. All data result will excute in onactivityresult of FragmentParent

follow this link: Receive result from DialogFragment

Electronics answered 5/3, 2014 at 7:46 Comment(1)
While this is correct, it does nothing to a question. Because a transfer of data and restoring a callback after rotation are different things. For instance, in the callback I can open another activity or make a request.Acceptable
D
-7

Another way is that you can stop the activity getting recreated. You have to tell Android that you'll handle the orientation change yourself and android won't recreate your activity. You need to add this for your activity to your manifest file:

android:configChanges="keyboardHidden|orientation"

If not this, then you can use standard onSaveInstanceState() to save your state and recover using savedInstanceState as recommended by Google.

Here's Google's official guide for it: http://developer.android.com/guide/components/activities.html#Lifecycle

Go through it if you haven't already. It'll really help you in android development.

Doc answered 10/10, 2012 at 15:54 Comment(1)
I don't want to stop the activity being destroyed and recreated during a orientation change, its working as expected in this regard. I only want to know the best practice to handle the callbacks between the DialogFragment which was opened by a FragmentActivity during an orientation changes.Vesuvian

© 2022 - 2024 — McMap. All rights reserved.