ChatGPT解决这个技术问题 Extra ChatGPT

Receive result from DialogFragment

I am using DialogFragments for a number of things: choosing item from list, entering text.

What is the best way to return a value (i.e. a string or an item from a list) back to the calling activity/fragment?

Currently I am making the calling activity implement DismissListener and giving the DialogFragment a reference to the activity. The Dialog then calls the OnDimiss method in the activity and the activity grabs the result from the DialogFragment object. Very messy and it doesn't work on configuration change (orientation change) as the DialogFragment loses the reference to the activity.

Thanks for any help.

DialogFragments are still just fragments. Your approach is actually the recommended way for fragments to use to talk back to the main activity. developer.android.com/guide/topics/fundamentals/…
Thanks for that. I was very close (as you said). The bit that that linked document helped me with was using onAttach() and casting the activity to a listener.
@codinguser, @Styx - "giving the DialogFragment a reference to the activity" - this detail is a little risky, as both the Activity and the DialogFragment might be recreated. Using the Activity passed to onAttach(Activity activity) is the proper and recommended way.

w
whlk

Use myDialogFragment.setTargetFragment(this, MY_REQUEST_CODE) from the place where you show the dialog, and then when your dialog is finished, from it you can call getTargetFragment().onActivityResult(getTargetRequestCode(), ...), and implement onActivityResult() in the containing fragment.

It seems like an abuse of onActivityResult(), especially as it doesn't involve activities at all. But I've seen it recommended by official google people, and maybe even in the api demos. I think it's what g/setTargetFragment() were added for.


What if the target is an activity?
If target is activity I would declare interface with method like "void onActivityResult2(int requestCode, int resultCode, Intent data)" and implement it by an Activity. In DialogFragment just getActivity and check for this interface and call it appropriately.
It is not good solution. It will not working after save and restore dialog fragment state. LocalBroadcastManager is best solution in this case.
@Nik That's just not true. It's the best solution. There is no problem when saving and restoring the state. If you ever had a problem you used the wrong fragment manager. The target fragment / caller hast to use getChildFragmentManager() to show the dialog.
setTargetFragment is now deprecated, but the replacement FragmentManager.setFragmentResultListener (described in Pass data between fragments is still in alpha.
A
Aleksandar Stefanović

As you can see here there is a very simple way to do that.

In your DialogFragment add an interface listener like:

public interface EditNameDialogListener {
    void onFinishEditDialog(String inputText);
}

Then, add a reference to that listener:

private EditNameDialogListener listener;

This will be used to "activate" the listener method(s), and also to check if the parent Activity/Fragment implements this interface (see below).

In the Activity/FragmentActivity/Fragment that "called" the DialogFragment simply implement this interface.

In your DialogFragment all you need to add at the point where you'd like to dismiss the DialogFragment and return the result is this:

listener.onFinishEditDialog(mEditText.getText().toString());
this.dismiss();

Where mEditText.getText().toString() is what will be passed back to the calling Activity.

Note that if you want to return something else simply change the arguments the listener takes.

Finally, you should check whether the interface was actually implemented by the parent activity/fragment:

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    // Verify that the host activity implements the callback interface
    try {
        // Instantiate the EditNameDialogListener so we can send events to the host
        listener = (EditNameDialogListener) context;
    } catch (ClassCastException e) {
        // The activity doesn't implement the interface, throw exception
        throw new ClassCastException(context.toString()
                + " must implement EditNameDialogListener");
    }
}

This technique is very flexible and allow calling back with the result even if your don;t want to dismiss the dialog just yet.


This works great with Activity's and FragmentActivity's but if is the caller a Fragment?
I'm not sure I fully understand you. But it will work the same if the caller is a Fragment.
If the caller was a Fragment then you can do a few things: 1. Pass the fragment as a reference (Might not be a good idea because you might cause memory leaks). 2. Use the FragmentManager and call findFragmentById or findFragmentByTag it will get the fragments that exist in your activity. I hope it helped. Have a great day!
The problem with this approach is that fragment are not very good at retaining object since they are meant to be recreated, for instance try to change the orientation, the OS will recreate the fragment but the instance of the listener will not be available anymore
@LOG_TAG look at the @Timmmm's answer. setTargetFragment() and getTargetFragment() are magic.
B
Brandon

There is a much simpler way to receive a result from a DialogFragment.

First, in your Activity, Fragment, or FragmentActivity you need to add in the following information:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    // Stuff to do, dependent on requestCode and resultCode
    if(requestCode == 1) { // 1 is an arbitrary number, can be any int
         // This is the return result of your DialogFragment
         if(resultCode == 1) { // 1 is an arbitrary number, can be any int
              // Now do what you need to do after the dialog dismisses.
         }
     }
}

The requestCode is basically your int label for the DialogFragment you called, I'll show how this works in a second. The resultCode is the code that you send back from the DialogFragment telling your current waiting Activity, Fragment, or FragmentActivity what happened.

The next piece of code to go in is the call to the DialogFragment. An example is here:

DialogFragment dialogFrag = new MyDialogFragment();
// This is the requestCode that you are sending.
dialogFrag.setTargetFragment(this, 1);     
// This is the tag, "dialog" being sent.
dialogFrag.show(getFragmentManager(), "dialog");

With these three lines you are declaring your DialogFragment, setting a requestCode (which will call the onActivityResult(...) once the Dialog is dismissed, and you are then showing the dialog. It's that simple.

Now, in your DialogFragment you need to just add one line directly before the dismiss() so that you send a resultCode back to the onActivityResult().

getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, getActivity().getIntent());
dismiss();

That's it. Note, the resultCode is defined as int resultCode which I've set to resultCode = 1; in this case.

That's it, you can now send the result of your DialogFragment back to your calling Activity, Fragment, or FragmentActivity.

Also, it looks like this information was posted previously, but there wasn't a sufficient example given so I thought I'd provide more detail.

EDIT 06.24.2016 I apologize for the misleading code above. But you most certainly cannot receive the result back to the activity seeing as the line:

dialogFrag.setTargetFragment(this, 1);

sets a target Fragment and not Activity. So in order to do this you need to use implement an InterfaceCommunicator.

In your DialogFragment set a global variable

public InterfaceCommunicator interfaceCommunicator;

Create a public function to handle it

public interface InterfaceCommunicator {
    void sendRequestCode(int code);
}

Then when you're ready to send the code back to the Activity when the DialogFragment is done running, you simply add the line before you dismiss(); your DialogFragment:

interfaceCommunicator.sendRequestCode(1); // the parameter is any int code you choose.

In your activity now you have to do two things, the first is to remove that one line of code that is no longer applicable:

dialogFrag.setTargetFragment(this, 1);  

Then implement the interface and you're all done. You can do that by adding the following line to the implements clause at the very top of your class:

public class MyClass Activity implements MyDialogFragment.InterfaceCommunicator

And then @Override the function in the activity,

@Override
public void sendRequestCode(int code) {
    // your code here
}

You use this interface method just like you would the onActivityResult() method. Except the interface method is for DialogFragments and the other is for Fragments.


This approach will not work if target is Activity because you can't call its onActivityResult (from your DialogFragment) due to protected access level.
That is simply not true. I use this exact code in my projects. That is where I pulled it from and it works just fine. Please remember if you are having this protected access level issue you can change your access level for any method and class from protected to private or public if necessary.
Hi, you say you can call dialogFrag.setTargetFragment(this, 1) from an Activity, but this method receives a Fragment as first argument, so this couldn't be casted. Am I right ?
I'll post some responses for you all in a few to explain the activity stuff.
@Swift @lcompare you probably need to override onAttach(Context context) in your DialogFragment. Like so: @Override public void onAttach(Context context) { super.onAttach(context); yourInterface = (YourInterface) context; }
A
A J

Well its too late may be to answer but here is what i did to get results back from the DialogFragment. very similar to @brandon's answer. Here i am calling DialogFragment from a fragment, just place this code where you are calling your dialog.

FragmentManager fragmentManager = getFragmentManager();
            categoryDialog.setTargetFragment(this,1);
            categoryDialog.show(fragmentManager, "dialog");

where categoryDialog is my DialogFragment which i want to call and after this in your implementation of dialogfragment place this code where you are setting your data in intent. The value of resultCode is 1 you can set it or use system Defined.

            Intent intent = new Intent();
            intent.putExtra("listdata", stringData);
            getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, intent);
            getDialog().dismiss();

now its time to get back to to the calling fragment and implement this method. check for data validity or result success if you want with resultCode and requestCode in if condition.

 @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);        
        //do what ever you want here, and get the result from intent like below
        String myData = data.getStringExtra("listdata");
Toast.makeText(getActivity(),data.getStringExtra("listdata"),Toast.LENGTH_SHORT).show();
    }

s
stkent

For anyone still reading this: setTargetFragment() has been deprecated. It is now recommended to use the FragmentResultListener API like this:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setFragmentResultListener("requestKey") { key, bundle ->
        val result = bundle.getString("resultKey")
        // Do something with the result...
    }

    ...

    // Somewhere show your dialog
    MyDialogFragment.newInstance().show(parentFragmentManager, "tag")
}

Then in your MyDialogFragment set the result:

button.setOnClickListener{
    val result = "some string"
    setFragmentResult("requestKey", bundleOf("resultKey" to result))
    dismiss()
}

With the release of Fragment library 1.3.0 (developer.android.com/jetpack/androidx/releases/…) this will be the "most correct" answer. Currently it is only available through alpha releases which should not be used in production.
parentFragmentManager is important. It's easy to send childFragmentManager by accident which doesn't trigger setFragmentResultListener lambda
How to do the Result.ACTIVITY_CANCEL with a dialog fragment in this new way?
l
lcompare

Different approach, to allow a Fragment to communicate up to its Activity:

1) Define a public interface in the fragment and create a variable for it

public OnFragmentInteractionListener mCallback;

public interface OnFragmentInteractionListener {
    void onFragmentInteraction(int id);
}

2) Cast the activity to the mCallback variable in the fragment

try {
    mCallback = (OnFragmentInteractionListener) getActivity();
} catch (Exception e) {
    Log.d(TAG, e.getMessage());
}

3) Implement the listener in your activity

public class MainActivity extends AppCompatActivity implements DFragment.OnFragmentInteractionListener  {
     //your code here
}

4) Override the OnFragmentInteraction in the activity

@Override
public void onFragmentInteraction(int id) {
    Log.d(TAG, "received from fragment: " + id);
}

More info on it: https://developer.android.com/training/basics/fragments/communicating.html


Thanks for summarizing it so well. Just one note for others, the Android Devs tutorial suggests to override the public void onAttach of the fragment and do the activity casting there
Z
ZeWolfe15

One easy way I found was the following: Implement this is your dialogFragment,

  CallingActivity callingActivity = (CallingActivity) getActivity();
  callingActivity.onUserSelectValue("insert selected value here");
  dismiss();

And then in the activity that called the Dialog Fragment create the appropriate function as such:

 public void onUserSelectValue(String selectedValue) {

        // TODO add your implementation.
      Toast.makeText(getBaseContext(), ""+ selectedValue, Toast.LENGTH_LONG).show();
    }

The Toast is to show that it works. Worked for me.


I am not sure if it is a right way to do it, but it certainly works :)
Better use Interface rather than hard-coupling with concrete classes.
A
Adil Hussain

I'm very surprised to see that no-one has suggested using local broadcasts for DialogFragment to Activity communication! I find it to be so much simpler and cleaner than other suggestions. Essentially, you register for your Activity to listen out for the broadcasts and you send the local broadcasts from your DialogFragment instances. Simple. For a step-by-step guide on how to set it all up, see here.


I like that solution, is this considered to be a good or best practice in Android?
I really liked the tutorial, thank you for posting it. I do want to add that depending on what you are trying to accomplish either method may be more useful than the other. I would suggest the local broadcast route if you have multiple input/results being sent back to the activity from the dialog. I would recommend using the onActivityResult route if your output is very basic/simple. So, to answer the best practice question, it depends on what you are trying to accomplish!
@AdilHussain You're right. I made the assumption that people were using Fragments within their Activities. The setTargetFragment option is great if you're communicating with a Fragment and a DialogFragment. But you need to use the Broadcast method when it's an Activity calling the DialogFragment.
For the love of Foo, do not use Broadcasts!! It opens your application to a slue of security issues. Also I find that the worst android application I have to work on abuse broadcasts. Can you think of a better way to make code completely unusable? Now I am having to root out broadcast receivers, instead of a CLEAR line of code? To be clear there are uses for broadcoasts, but not in this context! NEVER in this context! Its just sloppy. Local or not. Callbacks are all you need.
As well as the Guava EventBus another option is the GreenRobot EventBus. I've not used the Guava EventBus but have used the GreenRobot EventBus and have had a good experience with it. Nice and simple to use. For a small example of how to architecture an Android application to use the GreenRobot EventBus, see here.
M
Malachiasz

Or share ViewModel like showed here:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}


public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

https://developer.android.com/topic/libraries/architecture/viewmodel#sharing_data_between_fragments


L
Lanitka

In my case I needed to pass arguments to a targetFragment. But I got exception "Fragment already active". So I declared an Interface in my DialogFragment which parentFragment implemented. When parentFragment started a DialogFragment , it set itself as TargetFragment. Then in DialogFragment I called

 ((Interface)getTargetFragment()).onSomething(selectedListPosition);

B
Bosko Mijin

In Kotlin

    // My DialogFragment
class FiltroDialogFragment : DialogFragment(), View.OnClickListener {
    
    var listener: InterfaceCommunicator? = null

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        listener = context as InterfaceCommunicator
    }

    interface InterfaceCommunicator {
        fun sendRequest(value: String)
    }   

    override fun onClick(v: View) {
        when (v.id) {
            R.id.buttonOk -> {    
        //You can change value             
                listener?.sendRequest('send data')
                dismiss()
            }
            
        }
    }
}

// My Activity

class MyActivity: AppCompatActivity(),FiltroDialogFragment.InterfaceCommunicator {

    override fun sendRequest(value: String) {
    // :)
    Toast.makeText(this, value, Toast.LENGTH_LONG).show()
    }
}
 

I hope it serves, if you can improve please edit it. My English is not very good


In my case the dialog is created from a fragment not activity so this solution does not work. But I like the smiley you put :)
Y
Yessy

if you want to send arguments and receive the result from second fragment, you may use Fragment.setArguments to accomplish this task

static class FirstFragment extends Fragment {
    final Handler mUIHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case 101: // receive the result from SecondFragment
                Object result = msg.obj;
                // do something according to the result
                break;
            }
        };
    };

    void onStartSecondFragments() {
        Message msg = Message.obtain(mUIHandler, 101, 102, 103, new Object()); // replace Object with a Parcelable if you want to across Save/Restore
                                                                               // instance
        putParcelable(new SecondFragment(), msg).show(getFragmentManager().beginTransaction(), null);
    }
}

static class SecondFragment extends DialogFragment {
    Message mMsg; // arguments from the caller/FirstFragment

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onViewCreated(view, savedInstanceState);
        mMsg = getParcelable(this);
    }

    void onClickOK() {
        mMsg.obj = new Object(); // send the result to the caller/FirstFragment
        mMsg.sendToTarget();
    }
}

static <T extends Fragment> T putParcelable(T f, Parcelable arg) {
    if (f.getArguments() == null) {
        f.setArguments(new Bundle());
    }
    f.getArguments().putParcelable("extra_args", arg);
    return f;
}
static <T extends Parcelable> T getParcelable(Fragment f) {
    return f.getArguments().getParcelable("extra_args");
}

A
Ayush Sth

On a dialog Fragment

class AbcDialogFragment(private val ondata: (data: String) -> Unit) :  DialogFragment() {}

Code to show the dialog from fragment/Activity

val abcDialogFragment = AbcDialogFragment(ondata = {data->  })
                
abcDialogFragment.show(requireActivity().supportFragmentManager, "TAG")

and in the dialog fragment, you can invoke the onData when dialog fragment is closed or any click listeners.


E
Evtim Georgiev

TL;DR - use this AppDialog class to both pass data into DialogFragment as well as get result out of it.

Detailed explanation:

Premise - Fragments get destroyed and recreated on config changes. View models hang around. When using a Dialog, it is recommended to wrap it in DialogFragment so that when the user rotates device and changes orientation the Dialog will not unexpectedly disappear (the DialogFragment will re-create it and re-display it).

Limitation (hence this question) - The way the DialogFragment works is it takes a class that it will need to re-instantiate on configuration changes - that means one can't have constructor parameters to the subclass to pass parameters, and typically one needs to make custom callbacks through a view model to pass back result of dialog. That typically means a new subclass for every dialog.

The solution - To help with all this, this custom AppDialog fragment comes to the rescue - the parameters are stored in-memory (similar to view model, you can think of it as a tiny custom view model that holds T in memory and uses it to re-create the dialog on config changes) until the dialog fragment is dismissed. The proper way to call back would be through a view model. If the fragment that shows the AppDialog, then you probably already have a view model and you can reference it from the lambda used to create the dialog - that means additional strong reference to the view model until the dialog fragment is dismissed.

Example - see the examples where a simple Dialog is refactored to use this AppDialog utility class to both receive a parameter and do a callback to viewModel to notify of result.

The helper class:


class AppDialog<T>: DialogFragment() {
    companion object {

        fun<T> buildDialog(params: T? = null, builder: AppDialogLambda<T>): AppDialog<T> {

            // Setup arguments
            val args = Bundle()
            args.putInt("key", pushDialogArgs(params, builder))

            // Instantiate
            val fragment = AppDialog<T>()
            fragment.arguments = args
            return fragment
        }

        // --------------------
        // Dialog Arguments

        private var lastKey: Int = 0
        private val dialogArgs = mutableMapOf<Int, Pair<Any?, AppDialogLambda<*>>>()

        private fun pushDialogArgs(params: Any?, builder: AppDialogLambda<*>): Int {
            dialogArgs[lastKey] = params to builder
            return lastKey++
        }

        private fun getDialogArgs(key: Int): Pair<Any?, AppDialogLambda<*>> {
            return dialogArgs[key]!!
        }

        private fun deleteDialogArgs(key: Int) {
            dialogArgs.remove(key)
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        // Get arguments
        val argKey = requireArguments().getInt("key")
        val (params, builder) = getDialogArgs(argKey)

        // We are getting back our arguments we passed AppDialog.buildDialog and
        // the type is guaranteed to be the same. Silence this warning
        @Suppress("UNCHECKED_CAST")
        return (builder as AppDialogLambda<T>)(this, params as T?)
    }

    override fun onDismiss(dialog: DialogInterface) {
        super.onDismiss(dialog)
        val argKey = requireArguments().getInt("key")
        deleteDialogArgs(argKey)
    }
}

Example usage (after):

        val info = mapOf("message" to "${error.description}\n\nPlease check your Internet connection and try again.")
        AppDialog.buildDialog(info) { fragment, params ->
            fragment.isCancelable = false // since we are in a DialogFragment
            AlertDialog.Builder(fragment.context)
                .setTitle("Terms Of Service Failed To Load")
                .setMessage(params!!["message"])
                .setPositiveButton("Retry") { _, _ ->
                    // Update the view model instead of calling UserTOSFragment directly 
                    // as the fragment may be destroyed and recreated
                    // on configuration changes. The viewModel will stay alive.
                    viewModel.onTermsOfServiceReload()
                }
                .setNegativeButton("Cancel") { _, _ ->
                    viewModel.onTermsOfServiceDeclined()
                    fragment.findNavController().popBackStack()
                }.create()
        }.show(parentFragmentManager, "TOS Failed Dialog")

Example usage (before): Without using DialogFragment (for illustration purposes, don't do this, this is bad practice as the dialog will be destroyed on config changes), code inside UserTOSFragment.kt - note code used to call directly the UserTOSFragment.loadContent() on retry. This has to be rewritten to instead call viewModel.onTermsOfServiceDeclined() in the above example:

        AlertDialog.Builder(context)
            .setTitle("Terms Of Service Failed To Load")
            .setMessage("${error.description}\n\nPlease check your Internet connection and try again.")
            .setPositiveButton("Retry") { _, _ ->
                loadContent()
            }
            .setCancelable(false)
            .setNegativeButton("Cancel") { _, _ ->
                viewModel.onTermsOfServiceDeclined()
                findNavController().popBackStack()
            }
            .show()

D
Dennis K

Just to have it as one of the options (since no one mentioned it yet) - you could use an event bus like Otto. So in the dialog you do:

bus.post(new AnswerAvailableEvent(42));

And have your caller (Activity or Fragment) subscribe to it:

@Subscribe public void answerAvailable(AnswerAvailableEvent event) {
   // TODO: React to the event somehow!
}