Как стать автором
Обновить

Комментарии 29

Только что были на вашем докладе «Навигация без боли и слёз» на DevFest в Новосибирске. Библиотека оказалась весьма интересной, очень понравился такой достаточно прозрачный поход к навигации. Возвращаемся домой, внедрять Cicerone в свои проекты. Спасибо, что делаете нашу жизнь лучше!

Рад, что заинтересовал вас!

Добрый день, доклад действительно за интересовал, оданако появилось несколько вопросов:
1) Ок, команда SystemMessage однозначно решала какие-то ваши проблемы в проекте, однако встает 2 вопроса 1: насколько это вообще зона ответственности навигатора 2: если все же его, то почему насколько скудно, тогда уж надо разрешать ему в принципе разруливать логику startActivityForResult, то есть позволять перебрасывать некоторые объекты между экранами.

2) Мне очень понравилась Ваша идея разруливать навигацию в стороне от вьюх, однако (огромное) ограничение, связаное с тем что Activity это не экран, ломает всю красоту (на мой взгляд), хотелось бы единообразно работать с любым стеком навигаций в не зависимости от того с чем ты работаешь (Activity, Fragment and so on), а так получается что фрагменты и Android View используют Ваш подход, а Activity нет. Не могли бы Вы рассказать в чем причина? Почему Активити это не экран?

Про команду SystemMessage.
Я специально выделил ей особое внимание, так как она только косвенно связана с навигацией. Можно обойтись и без нее, но в ряде случаев это будет не так изящно как с ней!
Пример: форма оплаты. Пользователь заполнил поля и нажал далее, но сервер вернул ошибку авторизации. Нам надо сказать об этом пользователю и перекинуть на экран входа. Без команды SystemMessage это можно сделать двумя способами:


  • на экране оплаты показать диалог и ждать нажатия кнопки ОК. Тут придется блокировать отмену этого диалога + очищать поля с секретной информацией, так как ошибка авторизрции может говорить о том, что приложение в чужих руках.
  • переходить на экран входа с дополнительной информацией о том, что надо при запуске показать сообщение. Это само по себе некрасиво, так как, по логике, экран входа не должен ничего иного делать, кроме как регистрировать пользователя.

Разруливание логики startActivityForResult оставлено на решение в частном порядке, так как эта логика доступна только в Activity и частично во Fragment'ах, а библиотека не делает разницы между разными видами View. Как я и говорил, Cicerone легко расширять, все в ваших руках.
Важный момент: в подходе MVP не предполагается обмена данными между View и даже между Presenter'ами. Это надо реализовывать через модель. Да, в андроиде Activity обладают механизмом startActivityForResult, но он не единственно возможный. Если речь о сторонних библиотеках или системных Activity, то я советую рассматривать результат запуска таких Activity как обычное пользовательское действие и передавать данные в свой презентер, где уже делать остальную обработку.


"Activity это не экран" — кто сказал? Activity может быть View без каких либо ограничений! После перехода на новое Activity, первое Activity очистит навигатор в методе onPause, а новое Activity установит свой навигатор в onResume. Здесь будет только один момент, обусловленный архитектурой андроида: Activity будет, с одной стороны, реализовывать View, а с другой, заниматься навигацией. (помните слайд с Франкенштейном?)

Я добавил в Sample приложение пример навигации на Activity. Welcome!

Без команды SystemMessage это можно сделать двумя способами

А еще есть третий способ, презентер, на который возвращаемся, подписывается на ивенты/обсервер модели о протухании авторизации.

Таким образом, при возврате на экран он примет состояние: показать ошибку авторизации

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


Еще хочу подчеркнуть важную деталь: SystemMessage создана для показа сообщений именно при навигации, то есть это можно рассматривать как некоторое состояние в навигации. Неправильно использовать эту команду просто для показа сообщений на экранах. Это ответственность Вью. SystemMessage не умеет обрабатывать клик или еще что-то, это только сообщение пользователю о случившемся переходе.

Этих четырёх команд на практике достаточно для построения любых переходов

Не очень понятно что делать, если после успешной аунтефикации мне нужно переходить на новый экран, а не возвращаться на предыдущий? Или как запретить возвращаться на один из предыдущих экранов (не запрещая переход к корневому)? Команда типа RemoveBackScreen(String screenKey) могла бы решить обе эти задачи, а также с ней можно реализовать команду Replace.

1) Если я вас правильно понял, то вы спрашиваете про такой случай:
до перехода (F — активный экран)
a -> b -> c -> d -> e -> F
после перехода (G — активный экран)
a -> b -> c -> d -> G
Это решается методом в роутере:


public void customTransition() {
  backTo("d");
  forward("g");
}

2) Запрещать возвращаться на экран? Это как? Зачем тогда его оставлять в цепочке?

1) Да. Так можно решить, но хотелось бы избавиться от перехода назад к d. Мы ведь на самом деле не хотим его отображать. Кроме того, откуда нам знать, что там d? У нас нет ограничений на место откуда запущена аутентификация.


2) Не надо оставлять его в цепочке. Но убрать нужно уже после того, как он оказался в ее середине. Абстрактный пример (не очень хороший дизайн, просто для примера):


  1. Зайти в профиль контакта
  2. Зайти в группу контактов
  3. Зайти в другой профиль
  4. Зайти в группу контактов
    ...
  5. Зайти опять в профиль контакта из пункта 1
  6. Удалить контакт
  7. Возвращаться назад до корневого экрана

При возврате в экран из пункта 1 показывать будет нечего (контакт уже удален). Хорошо бы удалить этот экран из цепочки сразу при удалении контакта.

1) Экран D показан не будет, так как смена экранов происходит быстрее. И все зависит от задачи. Можно сделать возврат не к конкретному экрану, а "на два назад", если экраны аутентификации попадают в общую цепочку. Такие случаи решаются расширением роутера. Для этого библиотека и создавалась гибкой.
Другой вариант: делать аутентификацию в отдельном активити с локальной цепочкой экранов.


2) Вы описываете типичный кейс бизнес логики. Если вы хотите, можно создать для таких случаев отдельную команду навигации. Повторюсь, что все для этого готово. Библиотека включает в себя необходимый минимум, если стараться учесть все кейсы, то маленькое решение превратится в гигантского монстра!


Возможно в будущем мы и добавим что-то еще, но на данный момент это не кажется критичным

Библиотека включает в себя необходимый минимум, если стараться учесть все кейсы, то маленькое решение превратится в гигантского монстра!

Все верно. Просто по моему мнению, вместо команды Replace хорошо бы иметь что-то вроде RemoveBackScreen. Библиотека останется маленькой, но станет более гибкой.

RemoveBackScreen не решить явным образом при навигации по Активити.


А если чрезмерная гибкость может создать неоправданные сложности, то лучше ограничится на Replace.


Попробуйте сделать RemoveBackScreen, а там посмотрим :)

Делали что-то такое в навигации Appercode, но там, конечно, вся эта навигация гвоздями прибита и все попроще. Cicerone — это более гибкое решение и мне это нравится.

Этих четырёх команд на практике достаточно для построения любых переходов

Тут скорее имелось в виду, что их достаточно для построения практически любых переходов.


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


Для меня самое прекрасное в либе как раз то, что она маленькая и гибкая.

не обойтись без зависимости от Context’a, который не хочется передавать в слой логики… рискуя получить утечки памяти (если забыть очистить ссылку)

А как можно получить утечку памяти, если Context все равно живет всегда? Или речь идет об Activity, а не о Context.getApplicationContext?

Речь о контексте необходимом для навигации, например Activity, у которого брать FragmentManager и так далее

Спасибо за отличное решение. Очень понравился подход к навигации. В процессе тестирования возникла одна проблема. Ваш пример использует библиотеку Moxy для реализации mvp. В связи с этим получается нестыковка такого плана, что Moxy повторяет команды для новой view, присоединяющейся к presenter. Команды по переключению фрагментов реализовал с помощью Вашей библиотеки. Но при пересоздании активити они не повторяются, т.е. мы теряем преимущество Moxy с их очередью команд.
Попытка переопределить Router и добавить очередь не увенчалась успехом, потому что, как я думаю, переопределять нужно ещё и CommandBuffer, именно в нем логично добавить очередь команд и повторять её.
А также необходимо очищать очередь в случае смены rootScreen.
Можете что-нибудь посоветовать?

Cicerone решает проблему передачи навигационных вызовов, но не сохранения стека навигации. Cicerone отвечает за то, что команды навигации гарантировано дойдут до навигатора, но не сохраняет их для полного восстановления состояния приложения. (представьте как восстановить цепочку из смеси Activity и Fragment'ов)
Мы не хотели писать свой FragmentManager + ActivityStack + и тд.
У себя мы тоже используем Moxy. А за восстановление навигации отвечает сам андроид.

В том и дело, что андроид не верно восстанавливает навигацию в некоторых случаях.
Попробую объяснить, что я пытаюсь сказать.
Мы исходим из того, что у нас есть уже реализованные простые команды. (С помощью них действительно можно реализовать большую кучу сценариев)
Я подумывал о том, каждая из команд может быть двух типов: 1) корневая команда (например, newRootScreen и newScreenChain) — она должна очищать очередь команд, т.к. предыдущие команды переходов бессмысленно повторять; 2) простая команда (например, navigateTo) — она просто заноситься в очередь.
Таким образом, мы получаем четкую очередь команд до корня и может даже нет смысла повторять их все при переподключении view к presenter, но мне кажется их стоит сохранять.
Допустим при нажатии назад, мы сможем подставить нужную команду из очереди.

Ничего вам не мешает хранить команды. Они все приходят в навигатор, здесь можно придумать какой-то массив команд и складывать его в бандл при умирании.
Такие вещи могут превратить библиотеку во фреймворк, то есть внутри будет много собственной логики о которой надо будет постоянно помнить, а кому-то она может не подойти и тогда придется опять переделывать.

Но при пересоздании активити они не повторяются, т.е. мы теряем преимущество Moxy с их очередью команд.

Что-то не совсем понятно, что вы имеете в виду, можете описать иначе?


Попытка переопределить Router и добавить очередь.

Очередь невыполненных команд живет в CommandBuffer. Для чего вам очередь в рутере?


А также необходимо очищать очередь в случае смены rootScreen.

А для чего?


Из того что я понял, вы пытаетесь восстановить состояние стека экранов при помощи Cicerone, а состояние экрана средствами Moxy. Но зачем вам это если FragmentManager и так восстановит состояние бекстека я не понимаю. Или вы делаете без фрагментов?

1) у меня есть активити и несколько фрагментов (переключение из BottomBar), при повороте экрана Moxy следит, чтобы была выделена нужная иконка фрагмента, а фрагмент может загрузиться не тот (команда открытия нужного фрагмента не повторяется)

2) я пытался унаследоваться от Router, чтобы добавить очередь, адекватного ничего не вышло. Очередь нужно добавлять в класс CommandBuffer. Там только очередь НЕВЫПОЛНЕННЫХ команд, а мне нужно повторить ВЫПОЛНЕННЫЕ при повороте экрана.

3) пока делаю с фрагментами, но в будущем может будет и без них. я просто предлагаю логику для реализации очереди

1) Думаю тут проблема не в либе и не в том, что надо восстанавливать команды. Не надо. Нужно разобраться почему не восстанавливается нужный фрагмент. Он должен восстанавливаться силами FragmentManager и android. Ищите проблему в своей реализации. Потому что то, о чем говорите вы все верно отрабатывает в sample-приложении. Можно уйти далеко по стеку фрагментов и повернуть и все восстановится. Силами cистемы. Даже при включенном "не сохранять действия".


2) Я понимаю, что вы хотели сделать. Реализовать сохранение состояния стека экранов сохраняя последовательность навигационных команд. Как Moxy cохраняет состояние вью. Но это задача для отдельной либы или форка. Мы же наоборот хотели, чтобы либа была простой. Если нужно что-то более сложное — посмотрите в сторону Flow и Conductor.


3) Библиотека не держит очередь в себе. Это было изначальной мыслью при ее создании. Она дает интерфейс команд, механизм их комбинирования и пересылки. Все. Реализовать команды, хнанение стека экранов, восстановление стека — это все задачи за переделом ответственности библиотеки. В случае с фрагментами все уже сделано в андроиде, а мы даем дефолтную имлементацию навигатора. В случае с вьюхами это надо будет все делать самостоятельно. Либо силами другой либы.

Я Вас понял. Спасибо за ответ. Поищу ошибки в реализации. Возможно посмотрю в сторону других решений.
Но Ваш проект определенно заслуживает внимания.

Спасибо за такую высокую оценку! Очень приятно.
А вам быстро разобраться, в чем проблема и приятного кодинга!

Для всех интересующихся: добавил в Sample приложение экран со сложной параллельной навигацией (в каждом табе свой стек экранов).

Попробовал вашу замечательную библиотеку в многомодульном приложении - пока вот разбираюсь) С версией 1.0 понятно как работать - строки есть во всех модулях, а оформив переходы в общем файле константами можно вызывать переход откуда угодно и куда угодно - вся навигация пройдет через app-модуль. А вот как работать с версией 7.1? Она в отличии от первой просить screen, а в примерах он еще и с фабрикой идет. Т.е. вынести их в тот же gradle.prorepties уже не получится. Мб я чего-то не понимаю и ларчик просто открывается?

Похоже, что я нашел ответы на свой вопросы. Перворначально я хотел сделать абсолютно гибкую навигацию, так чтобы каждый модуль мог переходить в любой другой. Именно поэтому первая версия библиотеке подходила - достаточно было указать ключ фрагмента и вуаля! Переход! Слабостью казалось только расшаривание этих ключей (не писать же их в каждом модуль отдельно?), но потом мне вспомнилось, что все модули будут иметь свои зависимости, а значит при переходе их нужно будет предоставить, что убило идею на корню. Тогда я вернулся к версии 7.1 и стал предоставлять Screen из апи даггер компонента дочернего модуля (сложно, но вчитайтесь). Сами переходы стали более жесткими - каждый модуль просит на вход в виде зависимости только нужные ему переходы в виде интерфейса с методами отдающими Screen. Зависимости формируются в апп модуле. В итоге: о всех фрагментах и всех переходах знает только апп модуль. Дочерние модули полностью инкапсулированы. Переходы (Screen) предоставляют дочерние модули, app только их сшивает в нужные интерфейсы через их же апи и передает следующим дочерним модулям. Я так же смотрел в сторону андроид навигации, но переделывать пришлось бы слишком много. Плюс у нас проект горизонтальной ориентации, а в навигации все экраны показывают вертикально и никак это не изменить. В общем как-то так. Спасибо за либу, буду делывать

Зарегистрируйтесь на Хабре, чтобы оставить комментарий