Pull to refresh

Comments 38

Я правильно понимаю, что вверху — «плохой» пример из 10 строчек реализации диалога, а в самом низу — то же самое, но на нескольких страницах, с медленной сериализацией и самописным DI, который является «хорошим» кодом? Как думаете, что будет проще перенести в другой проект — «плохой» или «хороший» пример?
Давайте по порядку. Начнем с сериализации: согласен, что при таком подходе появляются накладные расходы на сериализацию, но, на мой взгляд, сохранение и восстановление одного объекта (не обладающего внутренним состоянием) не приведет к падению производительности. Можно вместо Serializable использовать Pacrelable, если Вы хотите более быстрой сериализации. К тому же, если Android SDK предоставляет возможность сохранять в Bundle сериализуемые объекты, то почему бы нам этим не воспользоваться, а не прикрываться фразами типа «медленная сериализация».

Теперь по поводу «самописного DI». Если Вы внимательно читали статью, то я нигде не призывал отказываться от DI фреймворков, а наоборот предлагаю использовать подход, описанный в публикации, совместно c ними. Давайте возьмем для примера Dagger до появления функционала AndroidInjection, приходилось писать подобное (код с официального сайта):
public class FrombulationActivity extends Activity {
  @Inject Frombulator frombulator;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // DO THIS FIRST. Otherwise frombulator might be null!
    ((SomeApplicationBaseType) getContext().getApplicationContext())
        .getApplicationComponent()
        .newActivityComponentBuilder()
        .activity(this)
        .build()
        .inject(this);
    // ... now you can write the exciting code
  }
}

Я лишь предлагал спрятать создание активити компонента. Примерно так.
interface Provider {
    Builder getBuilder(FrombulationActivity activity);
}

class ProviderImpl implements Provider {
    @Override
    Builder getActivityComponent(FrombulationActivity activity) {
        return ((SomeApplicationBaseType) getContext().getApplicationContext())
            .getApplicationComponent()
            .newActivityComponentBuilder()
            .activity(activity)
            .build();
    }
}

public class FrombulationActivity extends Activity {
  @Inject Frombulator frombulator;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // DO THIS FIRST. Otherwise frombulator might be null!
    Provider provider = getIntent().getExtras().getSerializable(PROVIDER);
    provider.getActivityComponent(this)
        .inject(this);
    // ... now you can write the exciting code
  }
}

При таком подходе активити не знает откуда берется компонент. Если активити лежит в общей библиотеке, которую использую несколько приложений, то такой способ внедрения зависимостей очень удобен. А также при тестировании активити.
Просто померяйте любым методом сколько времени занимает сериализация Serializable. Он на порядки медленнее всего остального. Мой коммент был не об этом. Короткий и простой код гораздо более переносим чем «правильный» но огромный. Да, диалогфрагмент провоцирует на написание плохого кода, однако в самом начале приведена практически каноническая реализация. Никто по своей воле не будет делать сериализацию, инжекты и кучу бойлерплейт кода вокруг уже необходимого кода для решения задачи в стиле «показать попап да/нет».
«Каноническое» означает основанное на вере, что так должно быть. Но разве разработка не подразумевает поиск новых подходов?! Я лишь поделился подходом, а применять его или нет — на усмотрение каждого конкретного разработчика. В любом случае, спасибо за конструктивную критику.
Используйте EventBus (шину событий) и вы забудете о слушателях, фрагментах, активити и их жизненных циклах.
ИМХО, EventBus — не панацея. Я хотел сделать компонент как можно более независимым от разных библиотек, чтобы его можно было переиспользовать в других проектах (где этих библиотек может не быть и добавление их не одобрят).
К тому же, хоть EventBus добавляет гибкости во взаимодействии между компонентами, он также накладывает больше ответственности на разработчиков — чтобы приложение не превратилось в запутанный клубок из событий. Лично я предпочитаю не злоупотреблять рассылкой событий и использовать EventBus только при острой необходимости.
Событие — по своему определению независимо от всех библиотек/компонентов/пр. Полностью согласен, что EventBus накладывает ответственность на разработчика. Так наша задача и состоит в том, что бы учиться правильно проектировать. И где нужна асинхронность + отвязка от ui/гибкая(меняющееся) бизнес логика/разделение приложения на слои — то лучше использовать события, а где можно использовать последовательности при неизменной бизнес логике — то лучший инструмент Rx и подобные инструменты. Поэтому выбранный Вами пример решается ну очень просто — генерацией события с данными — и всего одна строчка кода.

Это худшее, что можно было посоветовать! Евентбас неявно связывает все компоненты между собой. Все компоненты в приложении могут слушать всех. А дебажить это дело…
Закопайте его и не вспоминайте

Если не представляете, что такое сервис-ориентированная архитектура, то события Вам никогда не понадобятся. Я понимаю, что для приложения типа галерея — оно может и не нужно, но есть класс приложений, в которой использование сервис-ориентированной архитектуры важно.

Я отлично представляю, где может понадобится рассылка сообщений компонентами. Но это не для общения между диалогом и активити. А именно в данном контексте я и не рекомендую слушать ваш совет.
Из-за использования инструментов в неправильных местах потом и получаются монстры, которых невозможно поддерживать и отлаживать.

Диалог — это независимая визуальная сущность, создание единого интерфейса для взаимодействия с ним, независимого от порождающей и принимающей стороны — это задача, которая для реализации может использовать события. Т.е. создав сервис, который может порождать Диалоги и установив единый интерфейс получения результатов (например через событие) — вы просто забудете, что такое диалоги и их различные реализации (DialogFragment/AlertDialog). Умение проектировать независимыми сервисами — тоже умение. И единый транспорт (шина событий) в данной архитектуре — важнейшая часть.
Например есть задача вывода сообщений. Реализуйте все сервисом. И если завтра вас попросят выводить все через Toast, а послезавтра — через SnackBar — то вы просто меняете настройку прямо в приложении. Добавьте серьезность в сообщения. Вы поменяете только сервис, не трогая интерфейс взаимодействия. Интерфейс взаимодействия с сервисом един — через одно единственное событие. Тоже самое с диалогами — одно входящее событие и одно исходящее событие.

Минус данного подхода, что вы отправляете сообщение в "комос", а кто его получит — неизвестно. Это влечет за собой проблемы отладки и связывает слушателей и источники событий. А кто эти источники и кто у них слушатели можно определить только глобальным поиском по проекту.
Это все равно что во всех интерфейсах передавать объекты типа Object и по мере их использования кастовать к нужным типам. А после этого заменить все интерфейсы на один:
interface Listener { fun doAction(obj: Object) }

Я специально обратил внимание на Серьезность сообщения. В общем случае она не нужна — и в случае отсутствия слушателей оно теряется — закрыли фрагмент и нам не важно, что сообщение «Привет Вася» пропало. Но если приложение серьезно — и нам важен результат, то сервис обязан иметь зарегистрированных подписчиков(слушателей). Не важно какого типа они будут — важно, что они имеют нужный нам интерфейс. Нет живых подписчиков — бога ради используйте сервис по типу e-mail(т.е. появился слушатель — отправили ему мыло). Самое главное — нам не нужно никуда передавать ссылки на что-то — мы передаем только данные. А сервис сам решает — как поступить в каждом случае — отбросить событие/передать/переслать(положить в очередь/кэш). Я понимаю — а что делать, если зарегистрированного несколько слушателей? Добавьте текущего. Т.е. как с квартирой — хотите — сами ищите ее, а хотите — просто пишите объявление и ждете. Каждый выбирает свое.
Стандартный пример при плохой связи — получили отлуп и нужно вывести сообщение об этом. Но пользователь уже давно ушел из данного фрагмента и он уже давно уничтожен. В случае сервиса у вас есть список живых фрагментов/активити и вы выбираете один — текущий, тот который в состоянии на экране и спокойно передаете данные ему. Нет живых — приложение в фоне — положите в очередь в сервис мыла.

Вы очень много пишите и уходите от темы в аналогии. Евентбас — зло для андроид приложений, так как он совершенно неконтролируем. Если проект маленький и его пилит один человек, который умудряется все держать в голове, то еще прокатит. Для серьезной разработки — это недопустимо.


Я думаю стоит завершить этот спор. Есди я вас не переубедил — это ваше дело. Но воздержитесь рекомендовать Евентбас кому-то еще.

Перед тем как давать рекомендации — просто посмотрите процент использования EventBus. Я думаю также будет Вам полезно почитать о событийно-ориентированном программировании/clean architecture и прочих аналогичных штуках. И как их используют в крупных компаниях.
habrahabr.ru/company/yamoney/blog/334500
habrahabr.ru/post/128772
Использование в крупных компаниях — не показатель. Я в исходниках андроида порой такое нахожу…

Дабы не быть голословным, достаточно безобидный пример:

Есть LocalSocket. В доках ничего не сказано про параметр timeout в методе connect(). В исходниках сказано, что он игнорится. Угадаете, что происходит на самом деле?

Что происходит на самом деле
Он кидает исключение!


Так что говнокодеры и тут и там :)
А если хотите вообще копнуть глубоко — почитайте про MIMD системы (системы потока данных)
И наверно сидите за компом, в котором нет контроллера прерываний или его разрабатывал один человек :)

И как рассылка глобального сообщения по всему приложению связана с темой статьи — получения результата с диалога/соседнего фрагмента? Какая-то демагогия получается.
А по вашей теме — как я понимаю, вы хотите какой-то глобальный AlertDialog показывать в любом месте приложения при, скажем, получении какого-то сообщения из некого WebSocket-а. Не сказал бы, что эта идея мне нравится, но допустим, надо. Вы хотите куда-то EventBus-ом слать сообщения. Только непонятно, куда и кто должен на это реагировать. А вот как это сделать нормально (насколько слово «нормально» вообще применимо при подобной задаче. Берёте, регистрируете ActivityLifecycleEvents И при смене Activity, видимой для пользователя, запоминаете ее, а при необходимости берёте и показываете на ней, что хотите. Хотите — берёте ее контекст и Alertdialog/toast показываете, а может, вообще в decorview что-то вставляйте. А нет активных — пихаете сообщение куда-то в очередь или что у вас там. И никаких безликих event-ов в пустоту.

Если не использовали никогда EventBus зачем такие комментарии? Например завтра вам скажут пересылать все на сервер или запихивать все в БД, то каким способом вам поможет ActivityLifecycleEvents. В статических системах, в которых расписано все и на весь цикл разработки и жизни приложения не место событийно-ориентированным системам. Но в динамических системах, в которых вы не знаете, что от вас могут потребовать завтра (например — медицинские системы) — то используют вообще EventBus на каждый поток.

А каким боком EventBus к базе данных и серверу? :) И с чего вы взяли, что я не использовал EventBus? Пока что кроме пространных суждений я ничего не увидел. Медицинские системы зачем-то уже приплели. Мы тут вроде под Android пишем, судя по хабу.

EventBus служит просто транспортом для доставки сообщений и ничего больше. И ничем иным она (шина событий) не будет. Событие — это сущность, которая переносит только данные и ничего другого в них нет. Т.е. мы получаем систему, объединенную одной транспортной системой. По аналогии — мы имеем единую федеральную транспортную систему. Мы можем перемещать по ней что угодно — главная ее задача — это обеспечение своевременной ДОСТАВКИ и ничего более. Если вы живете в поселке — вы можете не видеть ее всю жизнь. Но любая транспортная система связывает что-то. В нашем случае модули (сервисы). Модулю (сервису — не путать с сервисом андроида) глубоко плевать — откуда/кто/как доставил данные/как доставит результат — он только производит специфичные ему преобразования. Причем их динамически можно подгружать/выгружать. Т.е. обеспечивать в приложении сервис в зависимости от требований. Поэтому на них и строят динамические системы. Т.е. если завтра потребуется дополнительно перенаправить поток сообщений с экрана смарта на удаленный сервер — никаких заминок не происходит — вы просто дополнительно перенаправляете поток событий в другой сервис. Т.е. Шина событий + сервисно-ориентированная архитектура обеспечивает максимальную гибкость в проектировании и эксплуатации приложения не ограничивая приложение ничем. Уберите шину событий и будете применять слушатели, callbacks, intents, broadcasts — все это уже давно проходили.

По вашим словам поди и JavaScript (и любой не статически типизированный язык) лучше Java (любой статик), тем что в динамичном приложении нам не надо париться о конкретных типах, и мы можем на лету трактовать типы как удобнее. Ну-ну.
А про устойчивость и надежность вы думали? Статические анализаторы не просто так придумали.
Потоки событий подойдут в распределенных системах, потому что там иначе никак. Но не в мобильных приложениях, где надежность, скорость и простота отладки гораздо важнее чем написание абстрактных коней в вакууме, которые шлют события абы куда и принимаю абы откуда

Специалистам по надежности я прошу обратить внимание на Ada — язык специально для военных систем. И когда приводите примеры на Java всегда вспоминайте наш любимый NullPointerException. Также не хочется выслушивать лекции про оптимизацию приложения на предмет утечек памяти, хотя 20 лет назад нам пели песни о супер оптимизаторе памяти Java, который все делает за вас. Честно скажу г… а в Java Android полно. И если ваша задача — это максимум Галерея фоток со стандартным набором Dagger2 + Picasso), то некоторые разработчики пилят расширяемые системы на нескольких потоках.

Если я встречу подобный подход в чьих-то исходниках, то сразу поставлю диагноз: "архитектура головного мозга". Без обид.
С точки зрения оторванной от реальности теории — может и не очень хорошо, что диалог ищет своего слушателя, но если вспомнить, что после восстановления система (андроид) сама создает компоненты, то это вполне нормально. Надо просто помнить, что активити/фрагменты/диалоги — независимые сущности, которые должны сами о себе позаботиться.

>> А с каких это пор компонент должен заниматься поиском слушателя

Так принято.

Я статью до конца не читал, только до этого момента, но у вас там, кажется, утечка будет. Потому при нормальной реализации вы создаете сильную ссылку в onAttach(), и обнуляете ее в onDetach(), а так у вас останется сильная ссылка и активити утечет. Нужно викреф делать.

Короче, вы велосипед ненужный сделали. Первоначальный вариант — лучший.
UPD. Дочитал-таки. Теперь точно уверен, что это велосипед.

return App.get()

Апп-синглтон, конечно, не приведет к утечке, но это индикатор плохой архитектуры.
Поделитесь, пожалуйста, каким образом Вы обходитесь без Апп-синглтона при разработке приложения?
Просто не использую контекст там, где нет возможности получить его без статических инстансов.
Можете показать пример, в котором по-вашему не обойтись без аппа-синглтона?
Дело тут вовсе не в контексте. Давайте на простом примере. В приложении создан класс, ответственный за общение с сервером. Как правило, чтобы не создавать несколько инстансов такого класса используют либо синглтон, либо создают его в Application (руками или с помощью компонента даггера) и соответственно добираются к инстансу этого класса через Application. В примере выше как раз описывался примерно такой случай.
Согласен, что ссылку, сохраненную в onAttach(), нужно чистить в onDetach(), чтобы не было утечки памяти. И в реальном коде это было. Я не стал это копировать сюда, так как это напрямую не относилось к теме статьи.
Спасибо за статью!
В проекте, которым сейчас занимаюсь, с большущим легаси, как раз нужно было написать фрагмент, который бы мог возвращать результаты — кому-то через eventBus, кому-то через onActivityResult, кому-то через синглтоны (через даггер), где-то фрагмент бы открывался напрямую, где-то — с другой activity. При этом весь этот мусор во фрагмент тащить не хотелось, и ваше решение идеально подошло.
Спасибо за Ваш отзыв. Один такой комментарий уже оправдывает публикацию данной статьи. Рад, что моя идея приглянулась Вам.
Sign up to leave a comment.

Articles