5

I have CustomDialog class that extends DialogFragment. I override onCreateDialog method, to get custom dialog i wanted.

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    dialog = new Dialog(activity, styleId);
    view = activity.getLayoutInflater().inflate(layoutId, null);
    dialog.setContentView(view);
    if (listener != null) {
        listener.onViewInit(view, this);
    }
    return dialog;

}

This is custom dialog creation code. After view is inflated, I call listener method listener.onViewInit(view, this) of type OnViewInitListener which is interface and extends Serializable, to bind custom code to view (view texts, listeners and etc.) , so that on rotation i want lose my button press logic.

@Override
public void onSaveInstanceState(Bundle bundle) {
    bundle.putInt("layoutId", layoutId);
    bundle.putInt("styleId", styleId);
    bundle.putSerializable("listener", listener);
    super.onSaveInstanceState(bundle);

}

public RsCustomDialog setOnListenerAssignment(OnViewInitListener listener) {
    this.listener = listener;
    return this;
}

When I implement OnViewInitListener from Activity, on orientation change things work as expected: onCreateDialog is called every time fragment is recreated, and ther are no parcel errors, but when I press applications history button (on rightmost) android buttons
(source: cbsistatic.com)

I get this error:

10-09 11:09:38.256: E/AndroidRuntime(24153): FATAL EXCEPTION: main
10-09 11:09:38.256: E/AndroidRuntime(24153): java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = base.RsCustomDialog$OnClickListener)
10-09 11:09:38.256: E/AndroidRuntime(24153):    at android.os.Parcel.writeSerializable(Parcel.java:1279)
10-09 11:09:38.256: E/AndroidRuntime(24153):    at android.os.Parcel.writeValue(Parcel.java:1233)
10-09 11:09:38.256: E/AndroidRuntime(24153):    at android.os.Parcel.writeMapInternal(Parcel.java:591)
10-09 11:09:38.256: E/AndroidRuntime(24153):    at android.os.Bundle.writeToParcel(Bundle.java:1627)
10-09 11:09:38.256: E/AndroidRuntime(24153):    at android.os.Parcel.writeBundle(Parcel.java:605)
10-09 11:09:38.256: E/AndroidRuntime(24153):    at android.support.v4.app.FragmentState.writeToParcel(Fragment.java:133)

I guess this is because, when I implement OnViewInitListener from my activity, java implicitly puts activity variable in implemented object, and Parcel can't handle Activity parcelation.

Can anyone suggest how to deal with this problem, or advice a better solution.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109

4 Answers4

6

You can't serialize and restore a listener.

Serialization (including using Parcelable) saves the state of an object instance and deserialization puts that state into a new object instance.

Listeners don't have state - which is why your Parcelable implementation isn't saving or restoring anything. The listener variable is a reference to an object instance (one that is known to implement the listener's interface). If a new instance of that object is created (ex: due to rotation, or process being killed due to low memory), it doesn't help for the dialog to attempt restoring the pointer to the previous instance. The previous instance no longer exists and the newly created (correct) instance didn't exist at the time onSaveInstanceState was invoked.

Two possible alternatives:

  • If the listener is intended to be the Activity which the dialog is attached to, then that can be restored in the DialogFragment's onAttach method
  • If not, then the Activity can set the Fragment listener when it is created (or re-created). To get a reference to an automatically restored Fragment instance, one can use getFragmentManager().findFragmentById(id) or getFragmentManager().findFragmentByTag(tag)
Stan Kurdziel
  • 5,476
  • 1
  • 43
  • 42
  • The second option is a bit tricky though, as if the activity is re-created it will lose any stored listeners... unless it's stored using a static variable or in the application, but that's not good practice... – User May 19 '17 at 09:57
  • @lxx I agree, static variable isn't a good idea - can't think of a case where application instance makes sense either, but that doesn't mean it doesn't exist. My thought on second option was to either create a new listener instance or set a fragment instance retrieved from the FragmentManager as the listener – Stan Kurdziel May 19 '17 at 13:32
1

Your OnViewInitListener should be static and serializable and has all the serializable fields inside. If you reference Activity from it then you do it wrong. To overcome the issue you may:

  1. Reference the activity instance stored in static WeakReference variable which is populated when activity gets created.
  2. Use broadcast receivers
  3. Reregister listener when fragment gets restored with the new one and proper context.
Eugene Popovich
  • 3,343
  • 2
  • 30
  • 33
  • Hi, thanks for reply! I can tell that i have checked 1 and 3 from your list: 1)I reference variables from static activity reference, that is in BaseActivity. 3) I don't want reregister listener, my aim is to register it once, and then forget about it. On config change it should automatically restore listener. But I always use proper context in `CustomDialog` class. 2- I don't know what you mean by that, how it can help? – Giorgi EgoTwin Khutsishvili Oct 09 '14 at 08:47
0

you can write something in onResume and onStop what you listener unregister in onStop method ,register in onResume method

wnp
  • 19
-1

Well, I solved it! What I did is I implemented Parcable as follows:

public abstract class OnClickListener implements DialogInterface.OnClickListener, Parcelable {
    @Override
    public abstract void onClick(DialogInterface dialog, int which);

    @Override
    public void writeToParcel(Parcel dest, int flags) {

    }

    @Override
    public int describeContents() {
        return 0;
    }
}

So my ConfirmDialog code stays the same:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new AlertDialog.Builder(getActivity())
        .setTitle(title).setMessage(text)
        .setPositiveButton(R.string.yes, onYes)
        .setNegativeButton(R.string.no, onNo).create();
}

@Override
public void onSaveInstanceState(Bundle bundle) {
    super.onSaveInstanceState(bundle);
    bundle.putParcelable("onYes", onYes);
    bundle.putParcelable("onNo", onNo);
}

The only limitation is do not use auto variables that are not Parcelable in onClick method. Here is my example of showing this dialog:

       showConfirmDialog(getString(R.string.sure_want_to_exit), new base.dialog.OnClickListener() {

        @Override
        public void onClick(DialogInterface dialog, int which) {
            ((NewProtocol) getCurrentActivity()).exit = true;               
            getCurrentActivity().finish();
        }
    }, null);

getCurrentActivity() is static method that returns current active activity.

PLNech
  • 3,087
  • 1
  • 23
  • 52
  • 9
    Writing nothing to the parcel gains us nothing. This is not persisting or deserializing anything at all. You need to set the listener from the activity when instance state is restored. Do not persist a pointer to nothing. This answer is far worse than crashing because it will silently break working listeners in edge cases where memory limits are reached. This could result in huge bugs going unnoticed in production. – colintheshots May 09 '17 at 03:05
  • Why does it even work though? Actually, it also works without the explicit persisting in onSaveInstanceState. `getArguments().getParcelable("listener")` always returns the same object passed to the Bundle regardless of how many times the phone orientation is a changed. – Saad Farooq Nov 20 '17 at 22:17