DialogFragment listener that persists on configuration changes
Asked Answered
A

5

8

The scenario is as follows, I have a ViewPager that contains fragments, each of these fragment has some action that requires confirmation.

I proceed to create a DialogFragment targeting the fragment that knows also how to handle the result, however the fragment might be recreated before the user confirms or declines the dialog..

I could pass a lambda, or some other form of a listener to the dialog, which would then be called when user confirms dialog, but the problem is that if the device is then rotated, the lambda is lost, as it cannot be persisted on the bundle...

Only way I can think of is assign some UUID to the dialog, and connect the UUID in the application to the lambda, which is kept on Map inside the application, however this seems very sloppy solution..

I tried searching for existing solutions online, such as material-dialogs librarys sample, but most of the cases seem to dismiss the dialog on rotation, but this also seems like a sloppy solution, as the dialog might be a part of longer flow, such as

request purchase -> cancel -> show dialog with explanation -> purchase again if user wants to

where the state of flow would be lost, if we simply dismiss dialog on rotation

Arak answered 7/1, 2019 at 11:49 Comment(1)
The answer would have been setTargetFragment on the DialogFragment, had Googlers not deprecate it despite it working 100% of the time for this scenario.Neediness
A
6

If you pass anonymous lambda/Listener you will lose it after rotate but if you make your activity implement your listener and assign it in onAttach(context) method of fragment, it will be reassigned after activity recreate.

interface FlowStepListener {
    fun onFirstStepPassed()
    fun onSecondStepPassed()
    fun onThirdStepPassed()
}
class ParentActivity: Activity(), FlowStepListener {
    override fun onFirstStepPassed() {
        //control your fragments here
    }
    override fun onSecondStepPassed() {
        //control your fragments here
    }
    override fun onThirdStepPassed() {
        //control your fragments here
    }
}
open class BaseDialogFragment : DialogFragment() {
    var listener: FlowStepListener? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (context is FlowStepListener) {
            listener = context
        } else {
            throw RuntimeException("$context must implement FlowStepListener")
        }
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }
}
Adore answered 10/2, 2019 at 22:11 Comment(0)
E
1

The best way to handle dialogs that I found is with EventBus. You basically send events from dialogs and intercept them in Activities/Fragments.

You can assign IDs to dialogs upon instantiation and add this ID to events to distinguish between events from different dialogs (even if the dialogs are from the same type).

You can see how this scheme works and get some additional ideas by reviewing the code here. You can also find this helper class that I wrote useful (though be careful with it because this code is very old; for example, I no longer make dialogs retained).

For completeness of the answer, I'll post some snippets here. Note that these snippets already make use of the new FragmentFactory, so the dialogs have constructor arguments. That's relatively recent addition, so your code will probably not use it.

This might be an implementation of a dialog that shows some info and has one button. You'd want to know when this dialog is dismissed:

public class InfoDialog extends BaseDialog {

    public static final String ARG_TITLE = "ARG_TITLE";
    public static final String ARG_MESSAGE = "ARG_MESSAGE";
    public static final String ARG_BUTTON_CAPTION = "ARG_POSITIVE_BUTTON_CAPTION";

    private final EventBus mEventBus;

    private TextView mTxtTitle;
    private TextView mTxtMessage;
    private Button mBtnPositive;

    public InfoDialog(EventBus eventBus) {
        mEventBus = eventBus;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext());
        LayoutInflater inflater = LayoutInflater.from(getContext());
        View dialogView = inflater.inflate(R.layout.dialog_info_prompt, null);
        dialogBuilder.setView(dialogView);

        initSubViews(dialogView);

        populateSubViews();

        setCancelable(true);

        return dialogBuilder.create();
    }

    private void initSubViews(View rootView) {
        mTxtTitle = (TextView) rootView.findViewById(R.id.txt_dialog_title);
        mTxtMessage = (TextView) rootView.findViewById(R.id.txt_dialog_message);
        mBtnPositive = (Button) rootView.findViewById(R.id.btn_dialog_positive);

        // Hide "negative" button - it is used only in PromptDialog
        rootView.findViewById(R.id.btn_dialog_negative).setVisibility(View.GONE);

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

    }

    private void populateSubViews() {
        String title = getArguments().getString(ARG_TITLE);
        String message = getArguments().getString(ARG_MESSAGE);
        String positiveButtonCaption = getArguments().getString(ARG_BUTTON_CAPTION);

        mTxtTitle.setText(TextUtils.isEmpty(title) ? "" : title);
        mTxtMessage.setText(TextUtils.isEmpty(message) ? "" : message);
        mBtnPositive.setText(positiveButtonCaption);
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        mEventBus.post(new InfoDialogDismissedEvent(getDialogTag()));
    }
}

And this dialog offers the user a choice between two options:

public class PromptDialog extends BaseDialog {

    public static final String ARG_TITLE = "ARG_TITLE";
    public static final String ARG_MESSAGE = "ARG_MESSAGE";
    public static final String ARG_POSITIVE_BUTTON_CAPTION = "ARG_POSITIVE_BUTTON_CAPTION";
    public static final String ARG_NEGATIVE_BUTTON_CAPTION = "ARG_NEGATIVE_BUTTON_CAPTION";

    private final EventBus mEventBus;

    private TextView mTxtTitle;
    private TextView mTxtMessage;
    private Button mBtnPositive;
    private Button mBtnNegative;

    public PromptDialog(EventBus eventBus) {
        mEventBus = eventBus;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext());
        LayoutInflater inflater = LayoutInflater.from(getContext());
        View dialogView = inflater.inflate(R.layout.dialog_info_prompt, null);
        dialogBuilder.setView(dialogView);

        initSubViews(dialogView);

        populateSubViews();

        setCancelable(false);

        return dialogBuilder.create();
    }

    private void initSubViews(View rootView) {
        mTxtTitle = (TextView) rootView.findViewById(R.id.txt_dialog_title);
        mTxtMessage = (TextView) rootView.findViewById(R.id.txt_dialog_message);
        mBtnPositive = (Button) rootView.findViewById(R.id.btn_dialog_positive);
        mBtnNegative = (Button) rootView.findViewById(R.id.btn_dialog_negative);

        mBtnPositive.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
                mEventBus.post(new PromptDialogDismissedEvent(getDialogTag(), PromptDialogDismissedEvent.BUTTON_POSITIVE));
            }
        });

        mBtnNegative.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
                mEventBus.post(new PromptDialogDismissedEvent(getDialogTag(), PromptDialogDismissedEvent.BUTTON_NEGATIVE));
            }
        });
    }

    private void populateSubViews() {
        String title = getArguments().getString(ARG_TITLE);
        String message = getArguments().getString(ARG_MESSAGE);
        String positiveButtonCaption = getArguments().getString(ARG_POSITIVE_BUTTON_CAPTION);
        String negativeButtonCaption = getArguments().getString(ARG_NEGATIVE_BUTTON_CAPTION);

        mTxtTitle.setText(TextUtils.isEmpty(title) ? "" : title);
        mTxtMessage.setText(TextUtils.isEmpty(message) ? "" : message);
        mBtnPositive.setText(positiveButtonCaption);
        mBtnNegative.setText(negativeButtonCaption);
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        dismiss();
        mEventBus.post(new PromptDialogDismissedEvent(getDialogTag(), PromptDialogDismissedEvent.BUTTON_NONE));
    }
}
Electrophorus answered 6/2, 2019 at 17:48 Comment(0)
C
0

Instead of using callbacks to capture a reference to your target object, try LocalBroadcastManager (docs).

The main advantages of this method are:

  1. No extra dependencies for your project, because LocalBroadcastManager is part of both the support-v4 and or AndroidX's legacy-support-v4, which you most likely already have.

  2. No need to keep any kind of references.

In a nutshell:

  • In the DialogFragment instead of invoking a callback , you send an Intent with a message through the LocalBroadcastManager, and
  • In your target Fragment, instead of passing a callback to the DialogFragment, you use a BroadcastReceiver to listen for messages tht come through LocalBroadcastManager.

For sending from within the DialogFragment:

public static final String MY_ACTION = "DO SOMETHING";

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
  final Button button = view.findViewById(R.id.accept);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent broadcastIntent = new Intent(MY_ACTION);
            LocalBroadcastManager.getInstance(getContext()).sendBroadcast(broadcastIntent);
            dismiss();
        }
    });
}

And for listening for messages in your target Fragment:

private final BroadcastReceiver localReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // Do whatever you need to do here
    }
};

@Override
protected void onStart() {
    super.onStart();

    final IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(MyDialogFragment.MY_ACTION);
    LocalBroadcastManager.getInstance(getContext())
        .registerReceiver(localReceiver, intentFilter);
}

@Override
protected void onStop() {
    super.onStop();
    LocalBroadcastManager.getInstance(this)
        .unregisterReceiver(localReceiver);
}
Constitutionalism answered 6/2, 2019 at 3:50 Comment(0)
R
0

You can use a ViewModel:

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

The documentation also addresses fragments in the section Share data between fragments.

...This common pain point can be addressed by using ViewModel objects. These fragments can share a ViewModel using their activity scope to handle this communication...

The interesting part may be:

Notice that both fragments retrieve the activity that contains them. That way, when the fragments each get the ViewModelProvider, they receive the same SharedViewModel instance, which is scoped to this activity.

See below how the viewModel survives the screen rotation. enter image description here

Rustin answered 11/2, 2019 at 10:16 Comment(1)
ViewModels are not supposed to hold a context. If the activity or parent fragment is the listener, then you are breaking that rule.Sultanate
T
0

Listener will produce some code coupling, In your case, Why not use event bus. Internally event bus works somewhat like listeners, but you don't have to manage anything yourself. Below are the steps to use event bus. Create an event Object ( It's better to keep it clean by an object )

    public class DialogDataEvent {

    String someData;  

    public DialogDataEvent(String data){
    this.someData=data;

}

    }

Then post your event

EventBus.getDefault().post(new DialogDataEvent("data"));

And receive it inside your Activity/Fragment

 @Subscribe
    public void onEvent(DialogDataEvent event) {
   //Do Some work here

    }

Do not forget to register and unregister your event bus in receiving class

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().register(this);
        }

    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }

And for MAMA Gradle :D

implementation "org.greenrobot:eventbus:3.1.1"
Thorianite answered 12/2, 2019 at 13:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.