从DialogFragment回调到Fragment

185

问题:如何从一个 DialogFragment 创建一个回调到另一个 Fragment。在我的情况下,涉及的 Activity 应该完全不知道 DialogFragment 的存在。

假设我有:

public class MyFragment extends Fragment implements OnClickListener

然后在某个时间点,我能够做到

DialogFragment dialogFrag = MyDialogFragment.newInstance(this);
dialogFrag.show(getFragmentManager, null);

MyDialogFragment看起来像:

protected OnClickListener listener;
public static DialogFragment newInstance(OnClickListener listener) {
    DialogFragment fragment = new DialogFragment();
    fragment.listener = listener;
    return fragment;
}

但如果DialogFragment暂停并通过其生命周期恢复,不能保证侦听器仍然存在。在Fragment中唯一的保证是通过setArguments和getArguments传递的Bundle。

如果应该是侦听器,则有一种引用活动的方式:

public Dialog onCreateDialog(Bundle bundle) {
    OnClickListener listener = (OnClickListener) getActivity();
    ....
    return new AlertDialog.Builder(getActivity())
        ........
        .setAdapter(adapter, listener)
        .create();
}

但我不希望Activity监听事件,我需要一个Fragment。 实际上,可以是任何实现OnClickListener接口的Java对象。

考虑一个具体的示例,一个通过DialogFragment呈现AlertDialog的Fragment。 它有Yes / No按钮。 我如何将这些按钮按下的情况发送回创建它的Fragment?


你提到:“但是不能保证如果DialogFragment在其生命周期中暂停和恢复,监听器仍然存在。” 我认为Fragment状态会在onDestroy()时被销毁?你可能是对的,但我有点困惑如何使用Fragment状态。我该如何重现你提到的问题,即监听器不存在? - Sean
我不明白为什么你不能在DialogFragment中简单地使用OnClickListener listener = (OnClickListener) getParentFragment();,而你的主Fragment像最初一样实现接口。 - kiruwka
这里有一个与问题无关的答案,但它确实展示了如何以一种干净的方式完成此操作。https://dev59.com/oYjca4cB1Zd3GeqPrAoC#33713825 - user2288580
18个回答

195

涉及的活动完全不知道DialogFragment。

片段类:

public class MyFragment extends Fragment {
int mStackLevel = 0;
public static final int DIALOG_FRAGMENT = 1;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState != null) {
        mStackLevel = savedInstanceState.getInt("level");
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("level", mStackLevel);
}

void showDialog(int type) {

    mStackLevel++;

    FragmentTransaction ft = getActivity().getFragmentManager().beginTransaction();
    Fragment prev = getActivity().getFragmentManager().findFragmentByTag("dialog");
    if (prev != null) {
        ft.remove(prev);
    }
    ft.addToBackStack(null);

    switch (type) {

        case DIALOG_FRAGMENT:

            DialogFragment dialogFrag = MyDialogFragment.newInstance(123);
            dialogFrag.setTargetFragment(this, DIALOG_FRAGMENT);
            dialogFrag.show(getFragmentManager().beginTransaction(), "dialog");

            break;
    }
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch(requestCode) {
            case DIALOG_FRAGMENT:

                if (resultCode == Activity.RESULT_OK) {
                    // After Ok code.
                } else if (resultCode == Activity.RESULT_CANCELED){
                    // After Cancel code.
                }

                break;
        }
    }
}

}

DialogFragment 类:

public class MyDialogFragment extends DialogFragment {

public static MyDialogFragment newInstance(int num){

    MyDialogFragment dialogFragment = new MyDialogFragment();
    Bundle bundle = new Bundle();
    bundle.putInt("num", num);
    dialogFragment.setArguments(bundle);

    return dialogFragment;

}

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {

    return new AlertDialog.Builder(getActivity())
            .setTitle(R.string.ERROR)
            .setIcon(android.R.drawable.ic_dialog_alert)
            .setPositiveButton(R.string.ok_button,
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getActivity().getIntent());
                        }
                    }
            )
            .setNegativeButton(R.string.cancel_button, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int whichButton) {
                    getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, getActivity().getIntent());
                }
            })
            .create();
}
}

104
我认为关键在于setTargetFragmentgetTargetFragment方法的使用。目前对于onActivityResult的使用不太清晰。最好在调用Fragment中声明自己特定的方法,并使用该方法,而不是重新定义onActivityResult。但这只是纯属语义上的问题。 - eternalmatt
2
堆栈级别的变量未被使用? - Display Name
10
这能经受配置更改或旋转吗? - Maxrunner
3
使用这个。备注:堆栈层级不需要在旋转或睡眠中继续存在。我的片段实现了DialogResultHandler#handleDialogResult(我创建的接口),而不是使用onActivityResult。@myCode,如果能展示选定对话框值被添加到Intent中并在onActivityResult中读取,将会非常有帮助。对于初学者,意图并不清晰。 - Chris Betti
8
@eternalmatt,你的反对完全合理,但我认为onActivityResult() 的价值在于它确保存在于任何Fragment上,因此可以使用任何Fragment作为父级。如果创建自己的接口并让父Fragment实现它,则只能将子Fragment与实现该接口的父Fragment一起使用。将子Fragment与该接口耦合可能会在以后更广泛地使用该子Fragment时带来麻烦。使用“内置”的onActivityResult() 接口不需要额外的耦合,因此允许您更灵活地使用它。 - Dalbergia
显示剩余5条评论

90

对话框片段并不是使用TargetFragment解决方案的最佳选项,因为在应用程序被销毁和重新创建后,它可能会导致IllegalStateException。 在这种情况下,FragmentManager找不到目标片段,并且您将收到一个像这样的带有消息的IllegalStateException

  

"android:target_state的片段不存在:索引1"

看起来Fragment#setTargetFragment()不适用于子片段与父片段之间的通信,而是适用于兄弟片段之间的通信。

因此,替代方法是如下所示使用父片段的ChildFragmentManager创建对话框片段,而不是使用活动的FragmentManager

dialogFragment.show(ParentFragment.this.getChildFragmentManager(), "dialog_fragment");

通过使用接口,在DialogFragmentonCreate方法中,您可以获取父片段:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    try {
        callback = (Callback) getParentFragment();
    } catch (ClassCastException e) {
        throw new ClassCastException("Calling fragment must implement Callback interface");
    }
}

接下来只需要在这些步骤之后调用您的回调函数即可。

如需了解更多信息,请查看以下链接: https://code.google.com/p/android/issues/detail?id=54520


3
这也适用于在API 23中引入的onAttach(Context context)方法。 - Santa Teclado
2
这个应该是被接受的答案。当前被接受的答案存在漏洞,而且不应该像这样使用片段。 - NecipAllef
3
问题在于如何正确获取片段之间的通信所需的上下文(父级)。如果您要将对话框片段用作活动的子级,则不应该有任何混淆。使用 Activity 的 FragmentManager 和 getActivity() 方法来检索回调足以解决这个问题。 - Oguz Ozcan
1
这应该是被接受的答案。它清晰地解释并详细说明,不仅仅是把代码扔给人们。 - Vince
1
@lukecross ParentFragment 是创建 DialogFragment 的 Fragment(调用 show() 方法的那个)。但是看起来 childFragmentManager 在重新配置/屏幕旋转时无法保存... - iwat0qs
显示剩余3条评论

40

我按照以下简单步骤完成了这项任务。

  1. 创建一个名为DialogFragmentCallbackInterface的接口,其中包含一些方法,例如callBackMethod(Object data)。您将调用此方法来传递数据。
  2. 现在,您可以在您的片段中实现DialogFragmentCallbackInterface接口,例如MyFragment implements DialogFragmentCallbackInterface
  3. 在创建DialogFragment时,设置调用片段MyFragment为目标片段,该片段创建了DialogFragment,使用myDialogFragment.setTargetFragment(this, 0)检查setTargetFragment (Fragment fragment, int requestCode)

MyDialogFragment dialogFrag = new MyDialogFragment();
dialogFrag.setTargetFragment(this, 1); 
  • 通过调用getTargetFragment()并将其转换为DialogFragmentCallbackInterface,将目标片段对象传入您的DialogFragment中。现在,您可以使用此接口向您的片段发送数据。

  • DialogFragmentCallbackInterface callback = 
               (DialogFragmentCallbackInterface) getTargetFragment();
    callback.callBackMethod(Object data);
    

    搞定了!只需要确保你在 fragment 中实现了这个接口即可。


    5
    这应该是最佳答案。很棒的回答。 - Md. Sajedul Karim
    请确保对源和目标碎片使用相同的 FragmentManager,否则 getTargetFragment 将无法使用。因此,如果您使用了 childFragmentManager,则它不会起作用,因为源碎片未由子碎片管理器提交。最好将这两个碎片视为兄弟碎片,而不是父/子碎片。 - Thupten
    坦白说,在两个兄弟Fragment之间通信时,最好只使用目标Fragment模式。通过不使用监听器,可以避免在Fragment2中意外泄漏Fragment1。当使用目标Fragment时,不要使用监听器/回调。只需使用onActivityResult(requestCode,resultCode,intent)将结果返回给Fragment1。从Fragment1中,使用setTargetFragment(),从Fragment2中使用getTargetFragment()。当使用父/子Fragment到Fragment或Activity到Fragment时,可以使用监听器或回调,因为在子Fragment中没有泄漏父Fragment的危险。 - Thupten
    1
    @Thupten,当你说“泄漏”时,是指内存泄漏还是“泄漏实现细节”?我认为Vijay的答案不会比使用onActivityResulty返回值更泄漏内存。这两种模式都将引用目标片段。如果你指的是泄漏实现细节,那么我认为他的模式甚至比onActivityResult更好。回调方法是显式的(如果命名正确)。如果你只得到OK和CANCELED,第一个片段必须解释它们的含义。 - tir38
    喜欢这个。以前从未听说过使用 setTargetFragment 来设置回调。非常好用! - sud007
    @tir38 没错。如果弹出 DialogFragment 并且没有其他操作,就不会发生泄漏。即使我们从 DialogFragment 启动一个新的 Fragment,旧的(第一个)仍然在内存中。这是因为 show() 调用了 FragmentManager 上的 add(),而不是 replace(),后者会删除以前的 Fragment。 - Michał Dobi Dobrzański

    36

    也许有点晚了,但可能会帮助像我一样有相同问题的其他人。

    在显示之前,您可以在Dialog上使用setTargetFragment,并且在对话框中,您可以调用getTargetFragment来获取引用。


    这是另一个问题的答案,但它也适用于你的问题,并且是一个干净的解决方案:https://dev59.com/oYjca4cB1Zd3GeqPrAoC#33713825 - user2288580
    非法状态异常 - luke cross

    33

    一种推荐的方法是使用新的Fragment Result API

    使用它时,您无需覆盖onAttach(context)setTargetFragment()因为该方法现在已过时


    1 - 在parent Fragment的onCreate中添加结果监听器:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        childFragmentManager.setFragmentResultListener("requestKey", this) { key, bundle ->
            val result = bundle.getString("bundleKey")
        }
    
    }
    

    2- 在片段中,设置结果(例如,在按钮点击侦听器上):

    button.setOnClickListener {
        val result = "resultSample"
    
        setFragmentResult("requestKey", bundleOf("bundleKey" to result))
    }
    

    更多信息请参阅文档:https://developer.android.com/guide/fragments/communicate#fragment-result

    希望能帮到你!


    5
    哇,多么好的答案啊!我想知道这个答案需要多长时间才能获得更多的投票。我是一名活跃的首席Android工程师,却不知道这个API,我必须说,这可能是2022年现在正确的方式,应该成为这个问题的被采纳答案!谢谢Geraldo :-) - Vin Norman
    1
    很高兴它有帮助!没有这个新API,做这么简单的任务真的很痛苦!干杯,Vin。 - Geraldo Neto
    1
    非常好的答案,谢谢。 - Chucky
    1
    这是最好的答案。 - Tarique Anowar
    它是否与“DialogFragment”兼容?由于某种原因,我在父片段中未收到回调。我正在使用导航组件在片段之间进行导航。 - Dionis Beqiraj
    显示剩余2条评论

    19

    与其他片段通信指南表示,Fragment应通过其关联的Activity进行通信。

    通常情况下,您需要一个Fragment与另一个Fragment通信,例如根据用户事件更改内容。所有Fragment之间的通信都是通过关联的Activity完成的。两个Fragment不应直接通信。


    1
    内部片段怎么办?也就是说,一个片段如何与宿主片段通信? - Ravi
    @EdwardBrey:在这种情况下,你如何组织实例状态键呢?例如,如果多个“基础”片段使用相同的DialogFragment实现? - Chris Betti
    1
    @Chris:如果片段需要持续通信,则为每个适当的片段定义一个接口以实现。然后,活动的工作仅限于为片段提供接口指针以与其对应的片段进行通信。之后,片段可以通过接口“直接”安全地进行通信。 - Edward Brey
    3
    随着片段的用途扩大,不使用直接片段通信的原始想法已经瓦解。例如,在导航抽屉中,活动的每个即时子片段都大致充当活动。因此,我认为让对话片段通过活动进行通信会损害可读性/灵活性。实际上,似乎没有任何好的方法来封装对话片段,以使其能够以可重用的方式与活动和片段一起工作。 - Sam
    17
    我知道这篇文章很老,但如果其他人来到这里,我觉得文中讨论的情况不适用于当一个片段“拥有”用于确定创建和管理 DialogFragment 的逻辑时。当活动甚至不确定为什么要创建对话框或在什么条件下应该解散它时,从片段到活动创建大量连接有点奇怪。除此之外,DialogFragment非常简单,只存在于通知用户并可能获取响应的目的。 - Chris
    显示剩余6条评论

    12

    你应该在你的碎片类中定义一个 interface,并在其父活动中实现该接口。具体细节可以参考这里:http://developer.android.com/guide/components/fragments.html#EventCallbacks。代码看起来类似于:

    Fragment:

    public static class FragmentA extends DialogFragment {
    
        OnArticleSelectedListener mListener;
    
        // Container Activity must implement this interface
        public interface OnArticleSelectedListener {
            public void onArticleSelected(Uri articleUri);
        }
    
        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            try {
                mListener = (OnArticleSelectedListener) activity;
            } catch (ClassCastException e) {
                throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
            }
        }
    }
    

    活动:

    public class MyActivity extends Activity implements OnArticleSelectedListener{
    
        ...
        @Override
        public void onArticleSelected(Uri articleUri){
    
        }
        ...
    }
    

    1
    我认为您读文档的速度过快。这两个代码片段都是“FragmentA”,他假定一个活动是一个“OnArticleSelectedListener”,而不是启动它的片段。 - eternalmatt
    2
    我认为你试图做的事情是不好的实践。Android指南建议所有片段之间的通信都通过活动进行(参见http://developer.android.com/training/basics/fragments/communicating.html)。如果你真的希望所有这些都在“MyFragment”内处理,你可能需要切换到使用常规的“AlertDialog”。 - James McCracken
    1
    我认为让片段直接相互通信的问题在于,在某些布局中,可能并未加载所有片段,并且如示例所示,可能需要切换片段。但是,当谈论从片段启动对话框片段时,我不认为这个问题是有效的。 - user486646
    1
    这是从架构角度来看的一个好实践,因此应该被接受作为答案。使用onActivityResult会导致意大利面条式的架构。 - Bruno Carrier
    这是与活动进行通信,而不是与片段进行通信,正如问题所要求的那样。 - xanexpt
    显示剩余2条评论

    10

    根据官方文档:

    Fragment#setTargetFragment

    此片段的可选目标。例如,如果此片段由另一个片段启动,并且完成后想将结果返回给第一个片段,则可以使用此选项。在此处设置的目标通过FragmentManager#putFragment跨实例保留。

    Fragment#getTargetFragment

    返回由setTargetFragment(Fragment, int)设置的目标片段。

    因此,您可以这样做:

    // In your fragment
    
    public class MyFragment extends Fragment implements OnClickListener {
        private void showDialog() {
            DialogFragment dialogFrag = MyDialogFragment.newInstance(this);
            // Add this
            dialogFrag.setTargetFragment(this, 0);
            dialogFrag.show(getFragmentManager, null);
        }
        ...
    }
    
    // then
    
    public class MyialogFragment extends DialogFragment {
        @Override
        public void onAttach(Context context) {
            super.onAttach(context);
            // Then get it
            Fragment fragment = getTargetFragment();
            if (fragment instanceof OnClickListener) {
                listener = (OnClickListener) fragment;
            } else {
                throw new RuntimeException("you must implement OnClickListener");
            }
        }
        ...
    }
    

    你能解释一下吗? - Yilmaz
    在这种情况下,我们需要将“MyFragment”引用传递给“MyDialogFragment”,而“Fragment”提供了执行此操作的方法。 我添加了官方文档的描述,它应该比我说得更清楚。 - SUPERYAO
    你已经使用了newInstance,不需要再次设置setTargetFragment。 - olajide
    setTargetFragment已被弃用 - NickUnuchek

    6
    更新:请注意,使用视图模型有更简单的方法来完成此操作,如果有人感兴趣,我可以分享。
    Kotlin小伙伴们,我们开始吧!
    所以我们面临的问题是,我们创建了一个活动,MainActivity,在该活动中我们创建了一个片段,FragmentA,现在我们想在FragmentA上创建一个对话框片段,称之为FragmentB。我们如何将FragmentB的结果传回到FragmentA而不经过MainActivity?
    注意:
    1. FragmentAMainActivity的子片段。为了管理在FragmentA中创建的片段,我们将使用childFragmentManager来完成这个任务!
    2. FragmentAFragmentB的父片段,为了从FragmentB内部访问FragmentA,我们将使用parentFragment

    话虽如此,在FragmentA内部,

    class FragmentA : Fragment(), UpdateNameListener {
        override fun onSave(name: String) {
            toast("Running save with $name")
        }
    
        // call this function somewhere in a clickListener perhaps
        private fun startUpdateNameDialog() {
            FragmentB().show(childFragmentManager, "started name dialog")
        }
    }
    

    这里是对话框片段 FragmentB
    class FragmentB : DialogFragment() {
    
        private lateinit var listener: UpdateNameListener
    
        override fun onAttach(context: Context) {
            super.onAttach(context)
            try {
                listener = parentFragment as UpdateNameListener
            } catch (e: ClassCastException) {
                throw ClassCastException("$context must implement UpdateNameListener")
            }
        }
    
        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
            return activity?.let {
                val builder = AlertDialog.Builder(it)
                val binding = UpdateNameDialogFragmentBinding.inflate(LayoutInflater.from(context))
                binding.btnSave.setOnClickListener {
                    val name = binding.name.text.toString()
                    listener.onSave(name)
                    dismiss()
                }
                builder.setView(binding.root)
                return builder.create()
            } ?: throw IllegalStateException("Activity can not be null")
        }
    }
    

    这是连接两者的界面。
    interface UpdateNameListener {
        fun onSave(name: String)
    }
    

    就是这样。


    1
    我按照这个文档进行操作:https://developer.android.com/guide/topics/ui/dialogs,但是它没有起作用。非常感谢。我希望这个parentfragment的东西每次都能像预期的那样工作 :) - UmutTekin
    1
    不要忘记在 onDetach() 方法中将监听器设置为 null :) - BekaBot
    @BekaBot 感谢您的评论。我进行了一些研究,似乎不必关闭监听器。https://dev59.com/FVoU5IYBdhLWcg3w35gM#37031951 - Gilbert

    5

    设置监听器到fragment的正确方法是在其附加时设置。我的问题在于onAttachFragment()从未被调用。经过一番调查,我意识到我一直在使用getFragmentManager而不是getChildFragmentManager

    以下是我的做法:

    MyDialogFragment dialogFragment = MyDialogFragment.newInstance("title", "body");
    dialogFragment.show(getChildFragmentManager(), "SOME_DIALOG");
    

    在onAttachFragment中附加它:

    @Override
    public void onAttachFragment(Fragment childFragment) {
        super.onAttachFragment(childFragment);
    
        if (childFragment instanceof MyDialogFragment) {
            MyDialogFragment dialog = (MyDialogFragment) childFragment;
            dialog.setListener(new MyDialogFragment.Listener() {
                @Override
                public void buttonClicked() {
    
                }
            });
        }
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接