【问题标题】:DialogFragment listener that persists on configuration changes在配置更改时持续存在的 DialogFragment 侦听器
【发布时间】:2019-01-07 11:49:42
【问题描述】:

场景如下,我有一个包含片段的 ViewPager,每个片段都有一些需要确认的动作。

我继续创建一个 DialogFragment,以同样知道如何处理结果的片段为目标,但是在用户确认或拒绝对话之前可能会重新创建片段。..

我可以将 lambda 或其他形式的侦听器传递给对话框,然后在用户确认对话框时调用它,但问题是如果设备随后旋转,则 lambda 会丢失,因为它不能被持久化在捆绑包中...

我能想到的唯一方法是将一些 UUID 分配给对话框,并将应用程序中的 UUID 连接到 lambda,该 lambda 保留在应用程序内部的 Map 上,但这似乎非常草率的解决方案..

我尝试在网上搜索现有的解决方案,例如material-dialogs librarys sample,但大多数情况下似乎会关闭轮换对话框,但这似乎也是一个草率的解决方案,因为对话框可能是较长流程的一部分,比如

请求购买 -> 取消 -> 显示带有解释的对话框 -> 如果用户愿意,请再次购买

如果我们简单地在旋转时关闭对话框,流的状态会丢失

【问题讨论】:

    标签: android android-fragments dialog android-dialogfragment


    【解决方案1】:

    如果你传递匿名 lambda/Listener,你会在旋转后丢失它,但如果你让你的活动实现你的监听器并在片段的 onAttach(context) 方法中分配它,它将在活动重新创建后重新分配。

    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
        }
    }
    

    【讨论】:

      【解决方案2】:

      我发现处理对话框的最佳方法是使用 EventBus。您基本上从对话框发送事件并在活动/片段中拦截它们。

      您可以在实例化时为对话框分配 ID,并将此 ID 添加到事件中以区分来自不同对话框的事件(即使对话框来自同一类型)。

      您可以通过查看the code here 了解此方案的工作原理并获得一些其他想法。您还可以找到我写的有用的this helper class(尽管要小心,因为这段代码很旧;例如,我不再保留对话框)。

      为了答案的完整性,我将在此处发布一些 sn-ps。请注意,这些 sn-ps 已经使用了新的 FragmentFactory,因此对话框具有构造函数参数。这是相对较新的添加,因此您的代码可能不会使用它。

      这可能是一个显示一些信息并有一个按钮的对话框的实现。您想知道此对话框何时关闭:

      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()));
          }
      }
      

      此对话框为用户提供了两个选项之间的选择:

      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));
          }
      }
      

      【讨论】:

        【解决方案3】:

        尝试使用LocalBroadcastManager (docs),而不是使用回调来捕获对目标对象的引用。

        这种方法的主要优点是:

        1. 您的项目没有额外的依赖项,因为 LocalBroadcastManager 是 support-v4 和/或 AndroidX 的 legacy-support-v4 的一部分,您很可能已经拥有它们。

        2. 无需保留任何类型的引用。

        简而言之:

        • 在 DialogFragment 中不是调用回调,而是通过LocalBroadcastManager 发送带有消息的Intent,并且
        • 在您的目标 Fragment 中,您无需将回调传递给 DialogFragment,而是使用 BroadcastReceiver 来侦听来自 LocalBroadcastManager 的消息。

        对于从 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();
                }
            });
        }
        

        并且用于监听目标 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);
        }
        

        【讨论】:

          【解决方案4】:

          您可以使用ViewModel

          ViewModel 类旨在以生命周期意识的方式存储和管理与 UI 相关的数据。 ViewModel 类允许数据在配置更改如屏幕旋转后仍然存在。

          该文档还解决了Share data between fragments 部分中的片段。

          ...这个常见的痛点可以通过使用 ViewModel 对象来解决。这些片段可以使用它们的活动范围共享一个 ViewModel 来处理这种通信......

          有趣的部分可能是:

          请注意,两个片段都会检索包含它们的活动。这样,当每个片段都获得 ViewModelProvider 时,它们会收到相同的 SharedViewModel 实例,该实例的作用域仅限于此活动。

          查看下面的 viewModel 如何在屏幕旋转后幸存下来。

          【讨论】:

          • ViewModel 不应该包含上下文。如果活动或父片段是侦听器,那么您就违反了该规则。
          【解决方案5】:

          监听器会产生一些代码耦合,在你的情况下,为什么不使用事件总线。内部事件总线的工作方式有点像侦听器,但您不必自己管理任何东西。以下是使用事件总线的步骤。 创建一个事件对象(最好用一个对象保持干净)

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

          然后发布您的活动

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

          并在您的 Activity/Fragment 中接收它

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

          不要忘记在接收类中注册和注销您的事件总线

          @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);
              }
          

          对于 MAMA Gradle :D

          implementation "org.greenrobot:eventbus:3.1.1"
          

          【讨论】:

            猜你喜欢
            • 2021-10-30
            • 1970-01-01
            • 2018-03-08
            • 1970-01-01
            • 2020-07-14
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多