Введение


В этой публикации я покажу, как можно передавать события из DialogFrament в вызывающий Fragment минуя Activity.

В официальном Guide по Dialogs есть раздел PassingEvents. В нем рассказано, как вернуть результат работы DialogFragment в вызывающую Activity. Для этого создается дополнительный интерфейс. Activity реализует этот интерфейс, а у DialogFrament есть ссылка на Activity.

Если инициатором вызова DialogFragment является другой Fragment, то в таком подходе нам придется сначала отправить результат в Activity, а потом уже из Activity в заинтересованный в данных Fragment. Отсюда и минусы:
  • Activity знает о деталях реализации фрагмента (сегодня это дополнительный диалог, а завтра можем реализовать все на одном фрагменте);
  • дополнительный код (больше кода = больше возможностей для ошибок).


К счастью, у класса Fragment есть 3 метода, которые позволят реализовать передачу событий из одного фрагмента в другой фрагмент минуя Activity. Это:

Суть метода


Вызывающий фрагмент:
  • с помощью setTargetFragment устанавливает себя в качестве targetFrament и устанавливает requestCode;
  • реализует метод onActivityResult в котором обрабатывает requestCode и resultCode, а также имеет доступ к дополнительным данным через intent.

Вызываемый фрагмент:
  • c помощью getTargetFrament получает ссылку на вызывающий фрагмент;
  • с помощью getTargetRequestCode получает код с которым он был вызыван;
  • вызывает onActivityResult вызывающего фрагмента и передает результаты своего выполнения через resultCode и Intent (для дополнительных).

Ниже пример кода. Для простоты оставил только актуальные для статьи части.

Вызывающий фрагмент:

public class HostFragment extends Fragment {
    private static final int REQUEST_WEIGHT = 1;
     private static final int REQUEST_ANOTHER_ONE = 2;
     public void openWeightPicker() {
        DialogFragment fragment = new WeightDialogFragment();
        fragment.setTargetFragment(this, REQUEST_WEIGHT);
        fragment.show(getFragmentManager(), fragment.getClass().getName());
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == Activity.RESULT_OK) {
            switch (requestCode) {
                case REQUEST_WEIGHT:
                         int weight = data.getIntExtra(WeightDialogFragment.TAG_WEIGHT_SELECTED, -1)
                         //используем полученные результаты
                         //...
                         break;
                    case REQUEST_ANOTHER_ONE:
                        //...
                         break;
                //обработка других requestCode
            }
            updateUI();
        }
    }
}

Вызываемый фрагмент:

public class WeightDialogFragment extends DialogFragment {
     //тэг для передачи результата обратно
    public static final String TAG_WEIGHT_SELECTED = "weight";
    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        LayoutInflater inflater = getActivity().getLayoutInflater();
        View view = inflater.inflate(..., null);
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setView(view)
                .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                             //отправляем результат обратно
                        Intent intent = new Intent();
                        intent.putExtra(TAG_WEIGHT_SELECTED, mNpWeight.getValue());
                        getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, intent);
                    }
                });
        return builder.create();
    }

}

Заключение


Перечисленные в начале статьи минусы в таком подходе отсутствуют. Только два взаимодействующих элемента знают друг о друге. Для остальных это уже неизвестные детали реализации.

Мне бы хотелось узнать, о таком варианте реализации еще при изучении официального гида по диалогам. Я же узнал позже благодаря StackOverflow.

Комментарии (13)


  1. iamironz
    08.06.2015 18:14
    +1

    А бывают ли реальные потребности отправлять события прямо во Activity? Может тогда и вызывать DialogFragment в самом Activity и имплементировать ему DialogFragment абстрактные методы?


    1. MyDogTom Автор
      08.06.2015 20:08

      А бывают ли реальные потребности отправлять события прямо во Activity?
      К примеру, если UI реализован в Activity без использования Fragment.
      Может тогда и вызывать DialogFragment в самом Activity и имплементировать ему DialogFragment абстрактные методы?
      На сколько я понимаю, речь об анонимном классе. Я бы такой подход не использовал т.к я вижу всего один сомнительный плюс и два существенных минуса.
      Сомнительный плюс: чуть меньше кода (не будет создания констант REQUEST_, не будет switch и не надо использовать intent).
      Минусы:
      • Анонимный класс хранит ссылку на внешний класс. Как результат, усложняется задача по обработке пересоздания фрагментов т.к. помимо сохранения/восстановления собственных данных нам нужно будет еще и эту ссылку воостанавливать. Решаемо конечно, но уже усложнение и дополнительная возможность ошибиться.
      • Весь код будет в вызывающих activity/fragment, а значит повторно использовать DialogFragment уже нельзя.


      1. KamiSempai
        08.06.2015 22:02

        На сколько я помню, при пересоздании анонимного диалог фрагмента выбрасывается исключение ClassNotFoundException. Так что это не просто сомнительно, а крайне не желательно.


      1. iamironz
        09.06.2015 09:03

        На сколько я понимаю, речь об анонимном классе.

        Нет, речь о абстрактном классе (не inner) DialogFragment, инстанс которого создается прямо в Activity по вызову такого же абстрактного метода во Fragment. Все эти абстрактные методы имплементируются на уровне инстанса, который нам надо слушать. По аналогии интерфейса.


        1. KamiSempai
          09.06.2015 11:55

          Вы говорите, что это не анонимный класс, но то, что вы описываете и есть анонимный класс.
          Если я правильно понял, вы имели ввиду вот такую реализацию:

          DialogFragment dialog = new AbsDialogFragment() {
              @Override
              public void someAbstractMethod(String someString) {
                  // Implementation ...
              }
          };
          dialog.show(getFragmentManager(), "ImplDialogFragment");

          Это яркий пример анонимного класса.


  1. KamiSempai
    08.06.2015 19:28

    Еще, для получения фрагмента, вызвавшего диалог, можно использовать метод getParentFragment(). Но для этого, при вызове диалога, нужно использовать ChildFragmentManager. Минус этого метода заключается в том, что для диалога нельзя будет установить setRetainInstance(true). Но как по мне так setRetainInstance(true) — это читерский прием, который может привести к кое-каким неприятностям в будущем, при уничтожении процесса.


    1. rude
      08.06.2015 20:57

      Думаю этот подход лучше. Использую его везде.
      Во все проекты добавляю что-то похожее на такой BaseDialogFragment


    1. MyDogTom Автор
      09.06.2015 13:34

      Считаю, что getParentFragment() хорош если у нас один вызывающий фрагмент и один вызываемый фрагмент (отношение один к одному). Если же у нас несколько вызываемых фрагментов (отношение один ко многим), то необходимо иметь возможность различить их с помощью getTargetRequestCode(), который устанавливается через setTargetFragment.


      1. KamiSempai
        09.06.2015 13:42

        RequestCode можно и через аргументы передать. А еще в ответе можно возвращать Tag фрагмента. Так что путей идентификации масса.


  1. Ghedeon
    08.06.2015 23:34

    Популярное решение для связей один-к-одному между фрагментами. К сожалению, не масштабируется на другие случаи вне фрагментов или когда нужно оповестить несколько компонентов системы. Как результат, чтобы не городить отдельную систему коллбэков для каждого отдельного случая, рано или поздно приходят к более универсальному event bus паттерну. Как бонус — объекты вместо intent.


  1. danikula
    09.06.2015 15:05

    А я комбинирую вариант имплементации кастомного интерфейса и вариант с использованием методов setTargetFragment и getTargetFragment. Выглядит это так: хост-фрагмент (вызывающий диалог) имплементирует интерфейс-колбек, далее в диалоге он устанавливается как target-фрагмент, при возврате результата getTargetFragment кастится к интерфейсу и дергается колбек. Плюсы такого подхода перед описываемым — отсутствие необходимости упаковывать-распаковывать результаты в intent, более очевидный и чистый код.


    1. danikula
      09.06.2015 15:24

      Полный код такого фрагмента тут
      Он позволяет создавать как простые фрагменты-диалоги типа

      new ConfirmDialogFragment.Builder(this)
          .title(R.string.menu_logout_confirm_dialog_title)
          .message(R.string.menu_logout_confirm_dialog_text)
          .show();
      

      … так и более сложные с передачей диалог-тега (для идентификации конкретного диалога в колбеке), установкой стиля, передачи дополнительных данных (cookie), которые будут доступны в колбеке.
      new ConfirmDialogFragment.Builder(this)
          .title(R.string.menu_logout_confirm_dialog_title)
          .message(R.string.menu_logout_confirm_dialog_text)
          .okButton(R.string.button_logout)
          .dialogTag("confirmLogout")
          .cookie("user", user)
          .destructive()
          .show();
      


      Все это нормально переживает пересоздание активити.


  1. r_ii
    09.06.2015 15:30
    +1

    Хорошее решение, но работает только, если оба фрагмента находятся в бек-стеке.
    Если это не будет удовлетворено, то фрагмент менеджер упадет при изменении конфигурации (target not in fragment manager).