Цель данной публикации — продемонстрировать подход к получению зависимостей во фрагментах, диалогах и активити.
Установка слушателя для диалога
В одном из проектов, перешедших по наследству, наткнулся на следующую реализацию диалога:
public class ExampleDialogFragment extends DialogFragment {
private Listener listener;
public interface Listener {
void onMessageEntered(String msg);
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
if(context instanceOf Listener) {
listener = (Listener) context;
} else {
listener = (Listener) getParentFragment();
}
}
}
«А с каких это пор компонент должен заниматься поиском слушателя» — думал я в тот момент.
Давайте сделаем так, чтобы фрагмент не знал, кто конкретно реализует интерфейс слушателя.
Многие могут сразу предложить, например, такой вариант:
public class ExampleDialogFragment extends DialogFragment {
private Listener listener;
public interface Listener {
void onMessageEntered(String msg);
}
public static DialogFragment newInstance(Listener listener) {
ExampleDialogFragment dialogFragment = new ExampleDialogFragment();
dialogFragment.listener = listener;
return dialogFragment;
}
}
и код активити, в которую будем встраивать данный диалог:
public class ExampleActivity extends AppCompatActivity {
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment
.newInstance(new DialogFragment.Listener() {
@Override
void onMessageEntered(String msg) {
// TODO
}
});
dialogFragment.show(getFragmentManager(), "dialog");
}
}
У данного решения есть один существенный недостаток. При изменении конфигурации (к примеру, переворот экрана) получим следующую цепочку: диалог сохранит свое состояние в
Bundle
и будет уничтожен -> активити будет удалена -> новый экземпляр активити будет создан -> диалог будет создан заново на основе сохраненного в Bundle
состояния. В итоге, мы потеряем ссылку на слушателя в диалоге, так как она явно не была сохранена и восстановлена. Мы, конечно, можем вручную вызвать setListener()
в одном из колбэков жизненного цикла активити, но есть и другой вариант. Так как анонимный класс мы не можем сохранить в Bundle
, равно как и экземпляры обычных классов, нам нужно нужно соблюсти следующие условия:- Слушатель должен реализовать интерфейс
Serializable
илиParcelable
- Передать слушателя через аргументы фрагмета при его создании
setArguments(Bundle args)
- Слушатель должен быть сохранен в методе
onSaveInstanceState(Bundle outState)
- Слушатель должен быть восстановлен в методе
Dialog onCreateDialog(Bundle savedInstanceState)
Как правило интерфейс слушателя реализуют такие компоненты Android как
Activity
или Fragment
. Подобные комоненты не предназначены для сохранения в Bundle
, поэтому нам надо найти другой подход к решению. Давайте попробуем передавать не самого слушателя, а «сыщика»(Provider), который способен его найти. В этом случае, нам никто не помешает сделать его сериализуемым и сохранять в Bundle
. Если наш «сыщик» не должен менять свое состояние в процессе взаимодействия с компонентом, то можно не переопределять метод
onSaveInstanceState(Bundle outState)
и при вызове метода Dialog onCreateDialog(Bundle savedInstanceState)
восстанавливать зависимость из аргументов.Давайте посмотрим на реализацию:
public class ExampleDialogFragment extends DialogFragment {
private static final String LISTENER_PROVIDER = "listener_provider";
private Listener listener;
public interface ListenerProvider extends Serializable {
Listener from(DialogFragment dialogFragment);
}
public interface Listener {
void onMessageEntered(String msg);
}
public static DialogFragment newInstance(ListenerProvider provider) {
ExampleDialogFragment dialogFragment = new ExampleDialogFragment();
Bundle args = new Bundle();
args.putSerializable(LISTENER_PROVIDER, provider);
dialogFragment.setArguments(args);
return dialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
if(args == null || !args.containsKey(LISTENER_PROVIDER)) {
throw new IllegalStateException("Listener provider is missing");
}
ListenerProvider listenerProvider =
(ListenerProvider) args.getSerializable(LISTENER_PROVIDER);
Listener listener = listenerProvider.from(this);
...
}
}
В таком случае код нашей активити примет вид:
public class ExampleActivity extends AppCompatActivity
implements ExampleDialogFragment.Listener {
@Override
public void onMessageEntered(String msg) {
// TODO
}
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment
.newInstance(new ListenerProvider());
dialogFragment.show(getFragmentManager(), "dialog");
}
private static class ListenerProvider
implements ExampleDialogFragment.ListenerProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
return (ExampleDialogFragment.Listener) dialogFragment.getActivity();
}
}
}
Если нам понадобится реализовать показ диалога из фрагмента, то получим слудеющий код:
public class ExampleFragment extends Fragment
implements ExampleDialogFragment.Listener {
@Override
public void onMessageEntered(String msg) {
// TODO
}
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment.newInstance(new ListenerProvider());
dialogFragment.show(getFragmentManager(), "dialog");
}
private static class ListenerProvider implements ExampleDialogFragment.ListenerProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
return (ExampleDialogFragment.Listener) dialogFragment.getParentFragment();
}
}
}
В итоге мы получаем, что вызывающий компонент сам помогает диалогу найти слушателя. При этом фрагмент понятия не имеет кто именно им (слушателем) является. К примеру, если по какой-либо причине нам не хочется обращаться к
Activity
напрямую, то никто нам не мешает просто кинуть событие, а затем в нужном месте поймать его и обработать (код диалога даже не потребуется менять):public class ExampleFragment extends Fragment {
void onMessageEvent(Message message) {
// TODO
}
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment.newInstance(new ListenerProvider());
dialogFragment.show(getFragmentManager(), "dialog");
}
private static class Message {
public final String content;
private Message(String content) {
this.content = content;
}
}
private static class ListenerProvider implements ExampleDialogFragment.ListenerProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
return new ExampleDialogFragment.Listener() {
@Override
public void onMessageEntered(String msg) {
EventBus.getDefault().post(new Message(msg));
}
};
}
}
}
С диалогами вроде разобрались. Едем дальше.
Поиск зависимостей во фрагментах
Часто приходится организовывать взаимодействие типа
Activity <-> Fragment
или Fragment <-> Fragment
в рамках одной активити. Общий принцип остается тот же, что был описан выше: посредством интерфейса (к примеру, Listener) и «сыщика» организуется общение между компонентами. В рамках данной статьи будем рассматривать одностороннее взаимодействие.В качестве примера, разберем случай с получением презентера во фрагменте. Думаю, каждый из нас сталкивался с подобным:
public interface Presenter {
...
}
public class ExampleFragment extends Fragment {
private Presenter presenter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
presenter = App.get().getExampleFragmentComponent().getPresenter();
}
}
И все вроде хорошо, создание зависимостей мы спрятали, а вот подобное их получение теперь разбросано по всему проекту. На мой взгляд, как минимум тот, кто вызывает, должен либо предоставить эти зависимости, либо помочь их найти.
Опять применим наш прием с «сыщиком»:
public class ExampleFragment extends Fragment {
private static final String DI_PROVIDER = "di_provider";
private Presenter presenter;
public interface DependencyProvider implements Serializable {
Presenter getPresenterOf(Fragment fragment);
}
public static Fragment newInstance(DependencyProvider dependencyProvider) {
Fragment fragment = new ExampleFragment();
Bundle args = new Bundle();
args.putSerializable(DI_PROVIDER, dependencyProvider);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if(args == null || !args.containsKey(DI_PROVIDER)) {
throw new IllegalStateException("DI provider is missing");
}
DependencyProvider diProvider =
(DependencyProvider) args.getSerializable(DI_PROVIDER);
presenter = diProvider.getPresenterOf(this);
}
}
public class ExampleActivity extends AppCompatActivity {
void showFragment() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment fragment = ExampleFragment
.newInstance(new DiProvider());
ft.add(R.id.container, fragment);
ft.commit();
}
private static class DiProvider
implements ExampleFragment.DependencyProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public Presenter get(Fragment fragment) {
return App.get().getExampleFragmentComponent().getPresenter();
}
}
}
Таким образом, мы сделали наш фрагмент более универсальным, соответственно, можем без проблем переносить его из проекта в проект не меняя код компонента без надобности.
Аналогичным способом можно организовать получение зависимостей в
Activity
. Небольшой пример с реализацией подобного подхода лежит здесь.
Надеюсь, описанный подход пригодится Вам в реализации проектов и принесет пользу.
Спасибо за внимание!
Комментарии (35)
oleg_shishkin
09.11.2017 22:45Используйте EventBus (шину событий) и вы забудете о слушателях, фрагментах, активити и их жизненных циклах.
ConstOrVar Автор
09.11.2017 22:56ИМХО, EventBus — не панацея. Я хотел сделать компонент как можно более независимым от разных библиотек, чтобы его можно было переиспользовать в других проектах (где этих библиотек может не быть и добавление их не одобрят).
К тому же, хоть EventBus добавляет гибкости во взаимодействии между компонентами, он также накладывает больше ответственности на разработчиков — чтобы приложение не превратилось в запутанный клубок из событий. Лично я предпочитаю не злоупотреблять рассылкой событий и использовать EventBus только при острой необходимости.oleg_shishkin
09.11.2017 23:52Событие — по своему определению независимо от всех библиотек/компонентов/пр. Полностью согласен, что EventBus накладывает ответственность на разработчика. Так наша задача и состоит в том, что бы учиться правильно проектировать. И где нужна асинхронность + отвязка от ui/гибкая(меняющееся) бизнес логика/разделение приложения на слои — то лучше использовать события, а где можно использовать последовательности при неизменной бизнес логике — то лучший инструмент Rx и подобные инструменты. Поэтому выбранный Вами пример решается ну очень просто — генерацией события с данными — и всего одна строчка кода.
terrakok
10.11.2017 10:44Это худшее, что можно было посоветовать! Евентбас неявно связывает все компоненты между собой. Все компоненты в приложении могут слушать всех. А дебажить это дело…
Закопайте его и не вспоминайтеoleg_shishkin
10.11.2017 11:19Если не представляете, что такое сервис-ориентированная архитектура, то события Вам никогда не понадобятся. Я понимаю, что для приложения типа галерея — оно может и не нужно, но есть класс приложений, в которой использование сервис-ориентированной архитектуры важно.
terrakok
10.11.2017 11:42Я отлично представляю, где может понадобится рассылка сообщений компонентами. Но это не для общения между диалогом и активити. А именно в данном контексте я и не рекомендую слушать ваш совет.
Из-за использования инструментов в неправильных местах потом и получаются монстры, которых невозможно поддерживать и отлаживать.oleg_shishkin
10.11.2017 12:07Диалог — это независимая визуальная сущность, создание единого интерфейса для взаимодействия с ним, независимого от порождающей и принимающей стороны — это задача, которая для реализации может использовать события. Т.е. создав сервис, который может порождать Диалоги и установив единый интерфейс получения результатов (например через событие) — вы просто забудете, что такое диалоги и их различные реализации (DialogFragment/AlertDialog). Умение проектировать независимыми сервисами — тоже умение. И единый транспорт (шина событий) в данной архитектуре — важнейшая часть.
oleg_shishkin
10.11.2017 12:24Например есть задача вывода сообщений. Реализуйте все сервисом. И если завтра вас попросят выводить все через Toast, а послезавтра — через SnackBar — то вы просто меняете настройку прямо в приложении. Добавьте серьезность в сообщения. Вы поменяете только сервис, не трогая интерфейс взаимодействия. Интерфейс взаимодействия с сервисом един — через одно единственное событие. Тоже самое с диалогами — одно входящее событие и одно исходящее событие.
terrakok
10.11.2017 12:32Минус данного подхода, что вы отправляете сообщение в "комос", а кто его получит — неизвестно. Это влечет за собой проблемы отладки и связывает слушателей и источники событий. А кто эти источники и кто у них слушатели можно определить только глобальным поиском по проекту.
Это все равно что во всех интерфейсах передавать объекты типа Object и по мере их использования кастовать к нужным типам. А после этого заменить все интерфейсы на один:
interface Listener { fun doAction(obj: Object) }oleg_shishkin
10.11.2017 13:06Я специально обратил внимание на Серьезность сообщения. В общем случае она не нужна — и в случае отсутствия слушателей оно теряется — закрыли фрагмент и нам не важно, что сообщение «Привет Вася» пропало. Но если приложение серьезно — и нам важен результат, то сервис обязан иметь зарегистрированных подписчиков(слушателей). Не важно какого типа они будут — важно, что они имеют нужный нам интерфейс. Нет живых подписчиков — бога ради используйте сервис по типу e-mail(т.е. появился слушатель — отправили ему мыло). Самое главное — нам не нужно никуда передавать ссылки на что-то — мы передаем только данные. А сервис сам решает — как поступить в каждом случае — отбросить событие/передать/переслать(положить в очередь/кэш). Я понимаю — а что делать, если зарегистрированного несколько слушателей? Добавьте текущего. Т.е. как с квартирой — хотите — сами ищите ее, а хотите — просто пишите объявление и ждете. Каждый выбирает свое.
oleg_shishkin
10.11.2017 13:23Стандартный пример при плохой связи — получили отлуп и нужно вывести сообщение об этом. Но пользователь уже давно ушел из данного фрагмента и он уже давно уничтожен. В случае сервиса у вас есть список живых фрагментов/активити и вы выбираете один — текущий, тот который в состоянии на экране и спокойно передаете данные ему. Нет живых — приложение в фоне — положите в очередь в сервис мыла.
terrakok
10.11.2017 13:29Вы очень много пишите и уходите от темы в аналогии. Евентбас — зло для андроид приложений, так как он совершенно неконтролируем. Если проект маленький и его пилит один человек, который умудряется все держать в голове, то еще прокатит. Для серьезной разработки — это недопустимо.
Я думаю стоит завершить этот спор. Есди я вас не переубедил — это ваше дело. Но воздержитесь рекомендовать Евентбас кому-то еще.
oleg_shishkin
10.11.2017 13:44Перед тем как давать рекомендации — просто посмотрите процент использования EventBus. Я думаю также будет Вам полезно почитать о событийно-ориентированном программировании/clean architecture и прочих аналогичных штуках. И как их используют в крупных компаниях.
habrahabr.ru/company/yamoney/blog/334500
habrahabr.ru/post/128772asmrnv777
10.11.2017 22:06Использование в крупных компаниях — не показатель. Я в исходниках андроида порой такое нахожу…
Дабы не быть голословным, достаточно безобидный пример:
Есть LocalSocket. В доках ничего не сказано про параметр timeout в методе connect(). В исходниках сказано, что он игнорится. Угадаете, что происходит на самом деле?
Что происходит на самом делеОн кидает исключение!oleg_shishkin
10.11.2017 13:51А если хотите вообще копнуть глубоко — почитайте про MIMD системы (системы потока данных)
oleg_shishkin
10.11.2017 13:56И наверно сидите за компом, в котором нет контроллера прерываний или его разрабатывал один человек :)
Bringoff
10.11.2017 20:33И как рассылка глобального сообщения по всему приложению связана с темой статьи — получения результата с диалога/соседнего фрагмента? Какая-то демагогия получается.
А по вашей теме — как я понимаю, вы хотите какой-то глобальный AlertDialog показывать в любом месте приложения при, скажем, получении какого-то сообщения из некого WebSocket-а. Не сказал бы, что эта идея мне нравится, но допустим, надо. Вы хотите куда-то EventBus-ом слать сообщения. Только непонятно, куда и кто должен на это реагировать. А вот как это сделать нормально (насколько слово «нормально» вообще применимо при подобной задаче. Берёте, регистрируете ActivityLifecycleEvents И при смене Activity, видимой для пользователя, запоминаете ее, а при необходимости берёте и показываете на ней, что хотите. Хотите — берёте ее контекст и Alertdialog/toast показываете, а может, вообще в decorview что-то вставляйте. А нет активных — пихаете сообщение куда-то в очередь или что у вас там. И никаких безликих event-ов в пустоту.oleg_shishkin
10.11.2017 21:30Если не использовали никогда EventBus зачем такие комментарии? Например завтра вам скажут пересылать все на сервер или запихивать все в БД, то каким способом вам поможет ActivityLifecycleEvents. В статических системах, в которых расписано все и на весь цикл разработки и жизни приложения не место событийно-ориентированным системам. Но в динамических системах, в которых вы не знаете, что от вас могут потребовать завтра (например — медицинские системы) — то используют вообще EventBus на каждый поток.
Bringoff
10.11.2017 21:52А каким боком EventBus к базе данных и серверу? :) И с чего вы взяли, что я не использовал EventBus? Пока что кроме пространных суждений я ничего не увидел. Медицинские системы зачем-то уже приплели. Мы тут вроде под Android пишем, судя по хабу.
oleg_shishkin
10.11.2017 22:34EventBus служит просто транспортом для доставки сообщений и ничего больше. И ничем иным она (шина событий) не будет. Событие — это сущность, которая переносит только данные и ничего другого в них нет. Т.е. мы получаем систему, объединенную одной транспортной системой. По аналогии — мы имеем единую федеральную транспортную систему. Мы можем перемещать по ней что угодно — главная ее задача — это обеспечение своевременной ДОСТАВКИ и ничего более. Если вы живете в поселке — вы можете не видеть ее всю жизнь. Но любая транспортная система связывает что-то. В нашем случае модули (сервисы). Модулю (сервису — не путать с сервисом андроида) глубоко плевать — откуда/кто/как доставил данные/как доставит результат — он только производит специфичные ему преобразования. Причем их динамически можно подгружать/выгружать. Т.е. обеспечивать в приложении сервис в зависимости от требований. Поэтому на них и строят динамические системы. Т.е. если завтра потребуется дополнительно перенаправить поток сообщений с экрана смарта на удаленный сервер — никаких заминок не происходит — вы просто дополнительно перенаправляете поток событий в другой сервис. Т.е. Шина событий + сервисно-ориентированная архитектура обеспечивает максимальную гибкость в проектировании и эксплуатации приложения не ограничивая приложение ничем. Уберите шину событий и будете применять слушатели, callbacks, intents, broadcasts — все это уже давно проходили.
terrakok
10.11.2017 23:37По вашим словам поди и JavaScript (и любой не статически типизированный язык) лучше Java (любой статик), тем что в динамичном приложении нам не надо париться о конкретных типах, и мы можем на лету трактовать типы как удобнее. Ну-ну.
А про устойчивость и надежность вы думали? Статические анализаторы не просто так придумали.
Потоки событий подойдут в распределенных системах, потому что там иначе никак. Но не в мобильных приложениях, где надежность, скорость и простота отладки гораздо важнее чем написание абстрактных коней в вакууме, которые шлют события абы куда и принимаю абы откудаoleg_shishkin
11.11.2017 10:20Специалистам по надежности я прошу обратить внимание на Ada — язык специально для военных систем. И когда приводите примеры на Java всегда вспоминайте наш любимый NullPointerException. Также не хочется выслушивать лекции про оптимизацию приложения на предмет утечек памяти, хотя 20 лет назад нам пели песни о супер оптимизаторе памяти Java, который все делает за вас. Честно скажу г… а в Java Android полно. И если ваша задача — это максимум Галерея фоток со стандартным набором Dagger2 + Picasso), то некоторые разработчики пилят расширяемые системы на нескольких потоках.
oleg_shishkin
11.11.2017 10:26И они рассматривают такие либы как
github.com/NewtronLabs/IpcEventbus?utm_source=android-arsenal.com&utm_medium=referral&utm_campaign=5710
terrakok
10.11.2017 10:50Если я встречу подобный подход в чьих-то исходниках, то сразу поставлю диагноз: "архитектура головного мозга". Без обид.
С точки зрения оторванной от реальности теории — может и не очень хорошо, что диалог ищет своего слушателя, но если вспомнить, что после восстановления система (андроид) сама создает компоненты, то это вполне нормально. Надо просто помнить, что активити/фрагменты/диалоги — независимые сущности, которые должны сами о себе позаботиться.
asmrnv777
10.11.2017 21:40>> А с каких это пор компонент должен заниматься поиском слушателя
Так принято.
Я статью до конца не читал, только до этого момента, но у вас там, кажется, утечка будет. Потому при нормальной реализации вы создаете сильную ссылку в onAttach(), и обнуляете ее в onDetach(), а так у вас останется сильная ссылка и активити утечет. Нужно викреф делать.
Короче, вы велосипед ненужный сделали. Первоначальный вариант — лучший.asmrnv777
10.11.2017 22:01UPD. Дочитал-таки. Теперь точно уверен, что это велосипед.
return App.get()
Апп-синглтон, конечно, не приведет к утечке, но это индикатор плохой архитектуры.ConstOrVar Автор
11.11.2017 12:14Поделитесь, пожалуйста, каким образом Вы обходитесь без Апп-синглтона при разработке приложения?
asmrnv777
12.11.2017 16:22Просто не использую контекст там, где нет возможности получить его без статических инстансов.
Можете показать пример, в котором по-вашему не обойтись без аппа-синглтона?
ConstOrVar Автор
11.11.2017 12:12Согласен, что ссылку, сохраненную в
onAttach()
, нужно чистить вonDetach()
, чтобы не было утечки памяти. И в реальном коде это было. Я не стал это копировать сюда, так как это напрямую не относилось к теме статьи.
Valle
Я правильно понимаю, что вверху — «плохой» пример из 10 строчек реализации диалога, а в самом низу — то же самое, но на нескольких страницах, с медленной сериализацией и самописным DI, который является «хорошим» кодом? Как думаете, что будет проще перенести в другой проект — «плохой» или «хороший» пример?
ConstOrVar Автор
Давайте по порядку. Начнем с сериализации: согласен, что при таком подходе появляются накладные расходы на сериализацию, но, на мой взгляд, сохранение и восстановление одного объекта (не обладающего внутренним состоянием) не приведет к падению производительности. Можно вместо
Serializable
использоватьPacrelable
, если Вы хотите более быстрой сериализации. К тому же, если Android SDK предоставляет возможность сохранять в Bundle сериализуемые объекты, то почему бы нам этим не воспользоваться, а не прикрываться фразами типа «медленная сериализация».Теперь по поводу «самописного DI». Если Вы внимательно читали статью, то я нигде не призывал отказываться от DI фреймворков, а наоборот предлагаю использовать подход, описанный в публикации, совместно c ними. Давайте возьмем для примера Dagger до появления функционала
AndroidInjection
, приходилось писать подобное (код с официального сайта):Я лишь предлагал спрятать создание активити компонента. Примерно так.
При таком подходе активити не знает откуда берется компонент. Если активити лежит в общей библиотеке, которую использую несколько приложений, то такой способ внедрения зависимостей очень удобен. А также при тестировании активити.
Valle
Просто померяйте любым методом сколько времени занимает сериализация Serializable. Он на порядки медленнее всего остального. Мой коммент был не об этом. Короткий и простой код гораздо более переносим чем «правильный» но огромный. Да, диалогфрагмент провоцирует на написание плохого кода, однако в самом начале приведена практически каноническая реализация. Никто по своей воле не будет делать сериализацию, инжекты и кучу бойлерплейт кода вокруг уже необходимого кода для решения задачи в стиле «показать попап да/нет».
ConstOrVar Автор
«Каноническое» означает основанное на вере, что так должно быть. Но разве разработка не подразумевает поиск новых подходов?! Я лишь поделился подходом, а применять его или нет — на усмотрение каждого конкретного разработчика. В любом случае, спасибо за конструктивную критику.