Введение
В этой публикации я покажу, как можно передавать события из DialogFrament в вызывающий Fragment минуя Activity.
В официальном Guide по Dialogs есть раздел PassingEvents. В нем рассказано, как вернуть результат работы DialogFragment в вызывающую Activity. Для этого создается дополнительный интерфейс. Activity реализует этот интерфейс, а у DialogFrament есть ссылка на Activity.
Если инициатором вызова DialogFragment является другой Fragment, то в таком подходе нам придется сначала отправить результат в Activity, а потом уже из Activity в заинтересованный в данных Fragment. Отсюда и минусы:
- Activity знает о деталях реализации фрагмента (сегодня это дополнительный диалог, а завтра можем реализовать все на одном фрагменте);
- дополнительный код (больше кода = больше возможностей для ошибок).
К счастью, у класса Fragment есть 3 метода, которые позволят реализовать передачу событий из одного фрагмента в другой фрагмент минуя Activity. Это:
- void setTargetFragment(Fragment fragment, int requestCode)
- Fragment getTargetFrament
- int getTargetRequestCode()
Суть метода
Вызывающий фрагмент:
- с помощью 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)
KamiSempai
08.06.2015 19:28Еще, для получения фрагмента, вызвавшего диалог, можно использовать метод getParentFragment(). Но для этого, при вызове диалога, нужно использовать ChildFragmentManager. Минус этого метода заключается в том, что для диалога нельзя будет установить setRetainInstance(true). Но как по мне так setRetainInstance(true) — это читерский прием, который может привести к кое-каким неприятностям в будущем, при уничтожении процесса.
rude
08.06.2015 20:57Думаю этот подход лучше. Использую его везде.
Во все проекты добавляю что-то похожее на такой BaseDialogFragment
MyDogTom Автор
09.06.2015 13:34Считаю, что getParentFragment() хорош если у нас один вызывающий фрагмент и один вызываемый фрагмент (отношение один к одному). Если же у нас несколько вызываемых фрагментов (отношение один ко многим), то необходимо иметь возможность различить их с помощью getTargetRequestCode(), который устанавливается через setTargetFragment.
KamiSempai
09.06.2015 13:42RequestCode можно и через аргументы передать. А еще в ответе можно возвращать Tag фрагмента. Так что путей идентификации масса.
Ghedeon
08.06.2015 23:34Популярное решение для связей один-к-одному между фрагментами. К сожалению, не масштабируется на другие случаи вне фрагментов или когда нужно оповестить несколько компонентов системы. Как результат, чтобы не городить отдельную систему коллбэков для каждого отдельного случая, рано или поздно приходят к более универсальному event bus паттерну. Как бонус — объекты вместо intent.
danikula
09.06.2015 15:05А я комбинирую вариант имплементации кастомного интерфейса и вариант с использованием методов setTargetFragment и getTargetFragment. Выглядит это так: хост-фрагмент (вызывающий диалог) имплементирует интерфейс-колбек, далее в диалоге он устанавливается как target-фрагмент, при возврате результата getTargetFragment кастится к интерфейсу и дергается колбек. Плюсы такого подхода перед описываемым — отсутствие необходимости упаковывать-распаковывать результаты в intent, более очевидный и чистый код.
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();
Все это нормально переживает пересоздание активити.
r_ii
09.06.2015 15:30+1Хорошее решение, но работает только, если оба фрагмента находятся в бек-стеке.
Если это не будет удовлетворено, то фрагмент менеджер упадет при изменении конфигурации (target not in fragment manager).
iamironz
А бывают ли реальные потребности отправлять события прямо во Activity? Может тогда и вызывать DialogFragment в самом Activity и имплементировать ему DialogFragment абстрактные методы?
MyDogTom Автор
На сколько я понимаю, речь об анонимном классе. Я бы такой подход не использовал т.к я вижу всего один сомнительный плюс и два существенных минуса.
Сомнительный плюс: чуть меньше кода (не будет создания констант REQUEST_, не будет switch и не надо использовать intent).
Минусы:
KamiSempai
На сколько я помню, при пересоздании анонимного диалог фрагмента выбрасывается исключение ClassNotFoundException. Так что это не просто сомнительно, а крайне не желательно.
iamironz
Нет, речь о абстрактном классе (не inner) DialogFragment, инстанс которого создается прямо в Activity по вызову такого же абстрактного метода во Fragment. Все эти абстрактные методы имплементируются на уровне инстанса, который нам надо слушать. По аналогии интерфейса.
KamiSempai
Вы говорите, что это не анонимный класс, но то, что вы описываете и есть анонимный класс.
Если я правильно понял, вы имели ввиду вот такую реализацию:
Это яркий пример анонимного класса.