Comments 46
Очень хороший подход! Очень забавно, что мы, создавая свою MVP либу, написали все до безобразия похоже. Я даже вижу проблемы, которые натолкнули вас на те или иные решения: тэгирование View, что бы иметь возможность переаттачить к ней презентер после поворота экрана, MVP-делегаты, что бы решить проблему отсутствия множественного наследования в Java и уже имеющиеся несопоставимые Fragment/DialogFragment, Activity/AppCompatActivity и т.п. (правда их мы подсмотрели в Nucleus), ViewState, что бы облегчить страдания при пересоздании View/Fragment/Activity из небытия, правда мы её назвали StateModel :)
Чем хочу поделиться из наших решений:
1) ViewState умеет сама записывать себя в Bundle savedState по умолчанию используя стандартный механизм серелизации java, но есть возможность повлиять на это переопределив в нужный метод в своей конкретной ViewState.
2) для связи между View -> Presenter -> ViewState -> View мы используем RxJava и её PublishSubject и BehaviorSubject, из которых легко строится очередь из событий и ожидание на их публикацию. Во-первых, это помогает вьюшке, подписавшись на изменения из View получить правильное состояние даже если ответ от сервера пришел как раз в тот момент, когда активити еще была в процессе пересоздания из-за поворота, например. Во-вторых, коммуникации между View, Presenter и ViewState защищены от эксепшенов в том смысле, что если что-то случится — мы то 100% вероятностью получим это в onError, даже если забыли поставить проверну на NPE. Ну и в-третьих, все подписки View на ViewState отписываются через делегат и по этому нигде ничего не течет :)
Чем хочу поделиться из наших решений:
1) ViewState умеет сама записывать себя в Bundle savedState по умолчанию используя стандартный механизм серелизации java, но есть возможность повлиять на это переопределив в нужный метод в своей конкретной ViewState.
2) для связи между View -> Presenter -> ViewState -> View мы используем RxJava и её PublishSubject и BehaviorSubject, из которых легко строится очередь из событий и ожидание на их публикацию. Во-первых, это помогает вьюшке, подписавшись на изменения из View получить правильное состояние даже если ответ от сервера пришел как раз в тот момент, когда активити еще была в процессе пересоздания из-за поворота, например. Во-вторых, коммуникации между View, Presenter и ViewState защищены от эксепшенов в том смысле, что если что-то случится — мы то 100% вероятностью получим это в onError, даже если забыли поставить проверну на NPE. Ну и в-третьих, все подписки View на ViewState отписываются через делегат и по этому нигде ничего не течет :)
У вас видимо ViewState хранится во View, поэтому вы вынуждены сериализовывать его и складывать в Bundle?
Мы решили развязать пользователю руки, и поэтому ссылка на ViewState хранится в Presenter. Presenter в свою очередь хранится не в Activity, а в статичном хранилище. Это позволяет не зависеть Presenter(а значит и ViewState) от жизненного цикла View. И поэтому даже если команда во ViewState прилетела в то время, когда View не приаттачена к Presenter/к ViewState, как только View будет приаттачена, ViewState сообщит ей весь набор команд, которые она должна выполнить. За счёт этого можно из Presenter передавать в командах даже несериализуемые данные.
А если вы это и говорили, то круто, что мы не одни так подумали =)
И да, у нас идёт тэгирование не View, а Presenter ;)
Мы решили развязать пользователю руки, и поэтому ссылка на ViewState хранится в Presenter. Presenter в свою очередь хранится не в Activity, а в статичном хранилище. Это позволяет не зависеть Presenter(а значит и ViewState) от жизненного цикла View. И поэтому даже если команда во ViewState прилетела в то время, когда View не приаттачена к Presenter/к ViewState, как только View будет приаттачена, ViewState сообщит ей весь набор команд, которые она должна выполнить. За счёт этого можно из Presenter передавать в командах даже несериализуемые данные.
А если вы это и говорили, то круто, что мы не одни так подумали =)
И да, у нас идёт тэгирование не View, а Presenter ;)
Да, если вдаваться в детали, мы положили ViewState в активити для того, что бы иметь возможность «вернуть все как было» не только при повороте экрана, но и при пересоздании всего процесса приложения (весьма частый кейс в Android 6 со своими новыми runtime пермишеннами). Конечно после таких издевательств над процессом в нем не останется никаких презентеров в статичном хранилище, а вот весь ActivityTask андроид нам любезно восстанавливает и отдает savedState, а там наша ViewState лежит себе :)
А для «легкого» пересоздания View, как в случае с поворотом, мы как раз так же используем хранилище презентеров. Вот только оно не статичное, а создается и лежит внутри Application. А тегирование для View мы используем не только для того, что бы автоматически переаттачить тот же презентер к новой View, но и для того, что бы разделять одинаковые Activity в одном AcivityTask и вешать им разные презентеры. Это на примерно такой случай: открыть активити «чатик с другом», из неё открыть активити «список друзей друга», а из неё открыть еще одну активити «чатик с еще одним другом». В итоге получим первую и последнюю активити одного класса, но чатик там должен быть разный, соответственно, и презентеры тоже разные.
А для «легкого» пересоздания View, как в случае с поворотом, мы как раз так же используем хранилище презентеров. Вот только оно не статичное, а создается и лежит внутри Application. А тегирование для View мы используем не только для того, что бы автоматически переаттачить тот же презентер к новой View, но и для того, что бы разделять одинаковые Activity в одном AcivityTask и вешать им разные презентеры. Это на примерно такой случай: открыть активити «чатик с другом», из неё открыть активити «список друзей друга», а из неё открыть еще одну активити «чатик с еще одним другом». В итоге получим первую и последнюю активити одного класса, но чатик там должен быть разный, соответственно, и презентеры тоже разные.
Понятно, а мы решили, что раз процесс убился, и всё-равно потерялись все Presenter, то просто пусть заново будет создан Presenter и всё начнётся сначала. Я замечал, что у стоковых Android-приложений именно такое поведение =)
Да, у нас тоже легко сделать кейс что на другой активити такого же типа будет использоваться другой Presenter =) Вообще, изначально все Presenter – локальные. И, соответственно, на каждый экран свои Presenter. А вот если указать глобальный тэг, то будет использоваться везде один Presenter. Ну и спец. фишка – динамический тэг для глобального презентера. Например, открыли список своих контактов → создался Presenter для нашего списка контактов. Затем открыли список контактов друга → создался Presenter для списка его контактов. Затем вернулись к своему списку контактов, и тут уже не создаётся новый Presenter, а берётся старый. Актуально может быть, например, если эти Presenter очень долго отрабатывают и будет обидно потерять их.
А ваше решение где-нибудь опубликовано? Было бы интересно посмотреть =)
Да, у нас тоже легко сделать кейс что на другой активити такого же типа будет использоваться другой Presenter =) Вообще, изначально все Presenter – локальные. И, соответственно, на каждый экран свои Presenter. А вот если указать глобальный тэг, то будет использоваться везде один Presenter. Ну и спец. фишка – динамический тэг для глобального презентера. Например, открыли список своих контактов → создался Presenter для нашего списка контактов. Затем открыли список контактов друга → создался Presenter для списка его контактов. Затем вернулись к своему списку контактов, и тут уже не создаётся новый Presenter, а берётся старый. Актуально может быть, например, если эти Presenter очень долго отрабатывают и будет обидно потерять их.
А ваше решение где-нибудь опубликовано? Было бы интересно посмотреть =)
Интересно, что вы еще написали себе аннотации. Лично мне не очень хотелось писать собственные кодогенерирующие аннотации для инжекта, на крайний случай хватает инжектов из даггера, но раз у вас есть, наверно стоит взглянуть на них тоже.
Ага, на внутрикомпанейском гитлабе :) Вероятно, когда нибудь оформим и в общий доступ
А ваше решение где-нибудь опубликовано?
Ага, на внутрикомпанейском гитлабе :) Вероятно, когда нибудь оформим и в общий доступ
Да, мы очень хотели, чтоб пришлось писать минимум кода. И в то же время хотелось попробовать annotation processor =) Результат крайне порадовал – для полноценного сохранения состояния достаточно применить аннотацию @GenerateViewState к MvpView и @InjectViewState к MvpPresenter. Когда видишь этот код и результат его работы, кажется что там есть магия =)
Правда, если можно обойтись без кодогенерации/рефлексии, используя только наследование/композицию, это наверное даже круче.
Правда, если можно обойтись без кодогенерации/рефлексии, используя только наследование/композицию, это наверное даже круче.
Вот как это можно сделать в moxy:
У этого способа есть минус – он не автоматизирован. Но есть и плюс – лишний раз Bundle парситься не будет. А вы как-нибудь автоматизировали создание сериализуемого ViewState?
- в каждом методе View сохранять в Bundle какое-то описание состояния
- складывать этот Bundle в outState
- в onCreate передавать этот Bundle в Presenter
- в Presenter смотреть в метод onFistViewAttached, есть ли Bundle
- если есть Bundle, «парсить» его и давать команды во ViewState
У этого способа есть минус – он не автоматизирован. Но есть и плюс – лишний раз Bundle парситься не будет. А вы как-нибудь автоматизировали создание сериализуемого ViewState?
У нас View только отражает состояние ViewState и передает клики и т.п. презентеру. ViewState изменяясь сообщает об этом View и View уже показывает прогрессы или пезультаты или еще чего. В onSaveInstanceState базоый презентер серелизует ViewState в бандл стандартным ObjectOutputStream и потом достает из бандла в onCreate() стандартным же ObjectInputStream. Этого хватает для большинства экранов. Если на каком то экране во ViewState требуется положить что-то такое, чего не стоит серелизовать этими средствами, то можно переопределить серелизацию/десерелизацию конкретно для этого экрана и пары ViewState-Presenter.
Подскажите, пожалуйста, как правильно передать данные из View в Presenter.
Предположим:
Если так сделать, то при каждом перевароте экрана, будет каждый раз отрабатывать presenter.start(value), а нужно, только один раз.
Предположим:
- есть ActivityTest
- из Intent-а получаем значение
- в Presenter
protected void onFirstViewAttach() { super.onFirstViewAttach(); getViewState().start(); }
- во View
@Override public void start() { presenter.start(getIntent().getExtras().getInt(Constants.VALUE)); }
Если так сделать, то при каждом перевароте экрана, будет каждый раз отрабатывать presenter.start(value), а нужно, только один раз.
Если вам нужно, чтоб команда отрабатывала исключительно один раз, значит она не должна быть сохранена во ViewState. Для этого у неё должна быть стратегия SkipStrategy. Её можно указать, применив к методу
Ещё на заметку, ваш код можно изменить:
Но это не обязательно – ваш подход абсолютно так же будет работать. Просто имейте ввиду возможность такого способа =)
Учтите, что метод
start
в интерфейсе View аннотацию: @StateStrategyType(SkipStrategy.class)Ещё на заметку, ваш код можно изменить:
- в activity, в методе
onCreate
выполните ваш кодpresenter.setStartValue(getIntent().getExtras().getInt(Constants.VALUE));
- в presenter, в методе
setStartValue
сохарните пришедшее значение где-нибудь в presenter - в методе
onFirstViewAttach
берёте это значение и работаете с ним
Но это не обязательно – ваш подход абсолютно так же будет работать. Просто имейте ввиду возможность такого способа =)
Учтите, что метод
onFirstViewAttach
будет вызван только при первом привязывании view. А после поворота девайса, он уже не будет вызван. Но похоже вы это и так поняли =)Как правильно поступить в такой ситуации:
Есть ViewPager в котором находятся 2 фрагмента (Fragment 1, Fragment 2). UI и логика фрагментов идентичны, отличие только в выборке данных из БД.
Вопрос: Можно как-то при переходе с одного фрагмента на другой обнулять/сбрасывать ViewState. Т.е. если на Fragment 1 было показано диалоговое окно и которое должно быть показано при перевороте устройства, то при переходе на другой фрагмент, Fragment 2 должен быть в первоночальном состоянии.
В такой ситуации нужно делать один presenter и при переходе между фрагментами обрабатывать события. Или лучше сделать 2 разных presenters, но тогда получим дублирование кода.
Есть ViewPager в котором находятся 2 фрагмента (Fragment 1, Fragment 2). UI и логика фрагментов идентичны, отличие только в выборке данных из БД.
Вопрос: Можно как-то при переходе с одного фрагмента на другой обнулять/сбрасывать ViewState. Т.е. если на Fragment 1 было показано диалоговое окно и которое должно быть показано при перевороте устройства, то при переходе на другой фрагмент, Fragment 2 должен быть в первоночальном состоянии.
В такой ситуации нужно делать один presenter и при переходе между фрагментами обрабатывать события. Или лучше сделать 2 разных presenters, но тогда получим дублирование кода.
В большинстве случаев, здесь будет достаточно сделать так, чтоб на каждую страницу ViewPager был свой Presenter. Так вам будет проще всего – не нужно будет ничего разруливать.
В таком случае, каждый Fragment будет по-своему инициализировать свой Presenter, а Presenter будет уже доставать нужные данные. И вам будет очень просто обработать команды из Presenter, и оба фрагмента будут независимы друг от друга.
В таком случае, каждый Fragment будет по-своему инициализировать свой Presenter, а Presenter будет уже доставать нужные данные. И вам будет очень просто обработать команды из Presenter, и оба фрагмента будут независимы друг от друга.
А вот предположим, мы хотим создать в своем коде отдельную сущность Router, тот самый, из соседней статьи про VIPER, и вызывать его методы из презентеров. Естественно, хочется обойтись без бойлерплейта, но не очень понятно как, роутеру для запуска активити нужен контекст — текущая активити.
Простое и некрасивое решение — явно вызывать Presenter.start(this) в OnCreate/OnStart. А есть ли решение красивее?
Простое и некрасивое решение — явно вызывать Presenter.start(this) в OnCreate/OnStart. А есть ли решение красивее?
Главный вопрос, который нам здесь нужно решить – а где будет жить экземпляр Router?
Если он будет жить и использоваться только во View, то никаких проблем нет – у нас есть напрямую доступ к Activity.
Сложней, если ссылка на Router нужна внутри Presenter. В таком случае вам не обойтись без явной передачи "чего-то" из View в Presenter. Здесь вы встаёте перед другим выбором: что передать из View? Context? А если Router будет не стартовать Activity, а менять фрагменты? Тогда придётся передавать что-то другое. Таким образом само собой напрашивается решение из соседней статьи: из View вы устанавливаете в Presenter непосредственно экземпляр Router, с которым в будущем будете работать из Presenter.
Мне кажется, если реализовывать VIPER, то нужно идти по второму пути и просто в onCreate передавать в Presenter экземпляр Router. В таком случае хотелось бы обратить внимание на две вещи:
PS: Router ломается, если вы начинаете строить приложение не на фрагментах, а на custom view, т.к. при смене конфигурации вы потеряете все изменения лэйаута. В таком случае не используйте Router, а работайте прямыми командами во View из Presenter через ViewState. Тогда вы не потеряете ваши изменения после изменения конфигурации.
Если он будет жить и использоваться только во View, то никаких проблем нет – у нас есть напрямую доступ к Activity.
Сложней, если ссылка на Router нужна внутри Presenter. В таком случае вам не обойтись без явной передачи "чего-то" из View в Presenter. Здесь вы встаёте перед другим выбором: что передать из View? Context? А если Router будет не стартовать Activity, а менять фрагменты? Тогда придётся передавать что-то другое. Таким образом само собой напрашивается решение из соседней статьи: из View вы устанавливаете в Presenter непосредственно экземпляр Router, с которым в будущем будете работать из Presenter.
Мне кажется, если реализовывать VIPER, то нужно идти по второму пути и просто в onCreate передавать в Presenter экземпляр Router. В таком случае хотелось бы обратить внимание на две вещи:
- Не забудьте убирать Router из Presenter, когда View уничтожается(иначе будет утечка памяти)
- Вы можете расширить функционал MvpDelegate, добавив в метод `onCreate` указывание Router для Presenter, и очищая ссылку на Router в Presenter внутри метода MvpDelegate `onDestroy`
PS: Router ломается, если вы начинаете строить приложение не на фрагментах, а на custom view, т.к. при смене конфигурации вы потеряете все изменения лэйаута. В таком случае не используйте Router, а работайте прямыми командами во View из Presenter через ViewState. Тогда вы не потеряете ваши изменения после изменения конфигурации.
Подскажите пожалуйста.
Если использовать Presenter для Fragment и во Fragment установить setRetainInstance(true); то при переворроте экрана, Presenter не востанавливает состояние View.
Если же убрать setRetainInstance(true); то при перевороте, происходит утечка памяти GC не освобождает память
Если использовать Presenter для Fragment и во Fragment установить setRetainInstance(true); то при переворроте экрана, Presenter не востанавливает состояние View.
Если же убрать setRetainInstance(true); то при перевороте, происходит утечка памяти GC не освобождает память
Интересное замечание. Действительно, состояние не будет восстановлено, т.к. у фрагмента не будет вызван метод onCreate(). Значит, в случае с retain-фрагментом можно поступить например так: вызывать метод делегата onCreate() не в onCreate() фрагмента, а где-нибудь в другом месте. Например, в методе onCreateView(). Правда, метод onAttach() может быть более подходящим местом, но я с ходу не могу ручаться за вызовы этого метода у retain-фрагментов.
А если делать не retain-фрагмент, то утечки памяти не должно быть – при вызове метода onDestroy() у фрагмента, он будет отвязан от презентера. В то же время в презентере хранятся weak references на View, поэтому утечки не должно быть. Может быть вы как-то самостоятельно храните ссылку на фрагмент где-нибудь в презентере?
А если делать не retain-фрагмент, то утечки памяти не должно быть – при вызове метода onDestroy() у фрагмента, он будет отвязан от презентера. В то же время в презентере хранятся weak references на View, поэтому утечки не должно быть. Может быть вы как-то самостоятельно храните ссылку на фрагмент где-нибудь в презентере?
Спасибо за советы!
Еще небольшой вопрос:
При загрузке данных — показываем диалоговое окно с прогрессом ( progress)
Что-то пошло не так, скрываем progress и показываем сообщение об ошибке ( error )
Пользователь прочитал сообщение об ошибке и закрыл error.
Так как это все делается через presenter, все сохраняется во view state
И при перевороте экрана отработают все 4 метода:
Отработает целых 4 метода ( при перевороте ), но по сути они отработают впустую, т.к. пользователь в такой ситуации не должен ничего увидеть.
Вопрос:
Можно и нужно ли как-то очищать очередь во view state?
Если будет вызвано гораздо больше методов, не приведет ли это к ненужной трате ресурсов?
При загрузке данных — показываем диалоговое окно с прогрессом ( progress)
Что-то пошло не так, скрываем progress и показываем сообщение об ошибке ( error )
Пользователь прочитал сообщение об ошибке и закрыл error.
Так как это все делается через presenter, все сохраняется во view state
И при перевороте экрана отработают все 4 метода:
- Показать progress
- Скрыть progress
- Показать error
- Скрыть error
Отработает целых 4 метода ( при перевороте ), но по сути они отработают впустую, т.к. пользователь в такой ситуации не должен ничего увидеть.
Вопрос:
Можно и нужно ли как-то очищать очередь во view state?
Если будет вызвано гораздо больше методов, не приведет ли это к ненужной трате ресурсов?
Для этого предусмотрены стратегии, применяются либо на весь view-интерфейс, либо на отдельные методы, можно писать свои. Подробнее в посте и исходниках.
Эти команды применятся очень быстро, что пользователь не почувствует, что применилось несколько команд.
Но в случае, если вы хотите, вы можете написать свою стратегию, которая будет удалять команду, к которой текущая является противодействием. Или же, если новая команда приводит View в такое состояние, что все предыдущие команды становятся точно не нужными, то можно применять стратегию SingleStateStrategy. Например если есть команда showData, и нет swipe to refresh, то можно к ней применить эту стратегию, т.к. после того, как установили данные, точно не нужно ни ошибку показывать, ни прогресс.
Но в случае, если вы хотите, вы можете написать свою стратегию, которая будет удалять команду, к которой текущая является противодействием. Или же, если новая команда приводит View в такое состояние, что все предыдущие команды становятся точно не нужными, то можно применять стратегию SingleStateStrategy. Например если есть команда showData, и нет swipe to refresh, то можно к ней применить эту стратегию, т.к. после того, как установили данные, точно не нужно ни ошибку показывать, ни прогресс.
@GenerateViewState — сейчас deprecated
День добрый.
Очень интересное поведение наблюдал на Android 6.
Есть TestActivity и Presenter. В приложении есть permission, например на использование камеры.
TestActivity запущена, метод onFirstViewAttach() — отработал.
Затем, пользователь заходит в Settings и включает/выключает permission.
Возращается в TestActivity и метод onFirstViewAttach опять отрабатывает.
Когда включаешь/отключаешь permission getMvpDelegate().onDestroy(); не отрабатывает.
Почему onFirstViewAttach() отрабатывает?
Очень интересное поведение наблюдал на Android 6.
Есть TestActivity и Presenter. В приложении есть permission, например на использование камеры.
TestActivity запущена, метод onFirstViewAttach() — отработал.
Затем, пользователь заходит в Settings и включает/выключает permission.
Возращается в TestActivity и метод onFirstViewAttach опять отрабатывает.
Когда включаешь/отключаешь permission getMvpDelegate().onDestroy(); не отрабатывает.
Почему onFirstViewAttach() отрабатывает?
Видимо, Android полностью останавливает приложение в таком случае. Тут вам не помогут даже глобальные presenter. Но похоже, что у activity должен был быть вызван метод onSaveInstanceState – в нём вы можете сохранить какие-нибудь флаги для presenter. А в onCreate передавать эти флаги в presenter(банально сделать метод init(Bundle args) в presenter), и уже в presenter решать, делать что-нибудь со view в onFirstViewAttach, или нет.
Переключение настроек полностью убивает процесс приложение и создает новый, по этому все презентеры тоже убиваются.
onSaveInstanceState()/onCreate() действительно вызываются, но не те, к которым мы привыкли, а новые, те что с 21 API: onSaveInstanceState(android.os.Bundle, android.os.PersistableBundle) и onCreate(android.os.Bundle, android.os.PersistableBundle)
onSaveInstanceState()/onCreate() действительно вызываются, но не те, к которым мы привыкли, а новые, те что с 21 API: onSaveInstanceState(android.os.Bundle, android.os.PersistableBundle) и onCreate(android.os.Bundle, android.os.PersistableBundle)
Может не внимательно прочитал, но не очень понял есть ли принципиальное отличие ViewState от механизма самого Андроида(onsaveinstancestate/onRestoreState) для случая связи одной View для одного Presenter'a?
Я даже не знаю, есть ли ясный ответ «да» или «нет». Могу только рассказать в чём разница, а вы уже сами определитесь =)
ViewState хранится в Presenter. Presenter хранится в static-хранилище. Поэтому нет никакой обходимости передавать в команды serializable-объекты. Можно складывать хоть что. Но в случае, если процесс будет уничтожен, то и static-хранилище с Presenter будет уничтожено. А значит и все ViewState будут уничтожены.
В то же время, в Bundle saveState можно складывать только serializable-объекты(ну и примитивы со String). Выигрыш, понятно, в том, что если процесс будет уничтожен, а потом восстановлен, мы сможем запросто достать команды из savedState. Но вот какая проблема: у вас может быть команда showProgress(). И если пользователь будет видеть прогресс, то наш Presenter обязан загрузить данные. А это значит мы должны во время применения команды, ещё и начать что-то делать в Presenter. Но велика вероятность, что для этого нам придётся сохранить оочень много информации в команде. А это чревато запутанным кодом. В то же время, если мы «лениво» сохраняем команды, то сперва из savesState будет получена команда showProgress(), а сразу после этого могут идти две команды hideProgress() и showData(). И тут придётся как-то очень сильно исхитриться, чтоб Presenter перестал грузить данные, т. к. они уже есть.
И такой подход с Bundle как раз используется в Mosby(ну или почти такой). И это мне в Mosby и не понравилось – нужда каждый раз руками разруливать восстановление состояния View.
Ещё, forceLain говорит, что они используют save state для хранения ViewState. Может, у них как-то по другому. Но я именно так вижу его использование =)
ViewState хранится в Presenter. Presenter хранится в static-хранилище. Поэтому нет никакой обходимости передавать в команды serializable-объекты. Можно складывать хоть что. Но в случае, если процесс будет уничтожен, то и static-хранилище с Presenter будет уничтожено. А значит и все ViewState будут уничтожены.
В то же время, в Bundle saveState можно складывать только serializable-объекты(ну и примитивы со String). Выигрыш, понятно, в том, что если процесс будет уничтожен, а потом восстановлен, мы сможем запросто достать команды из savedState. Но вот какая проблема: у вас может быть команда showProgress(). И если пользователь будет видеть прогресс, то наш Presenter обязан загрузить данные. А это значит мы должны во время применения команды, ещё и начать что-то делать в Presenter. Но велика вероятность, что для этого нам придётся сохранить оочень много информации в команде. А это чревато запутанным кодом. В то же время, если мы «лениво» сохраняем команды, то сперва из savesState будет получена команда showProgress(), а сразу после этого могут идти две команды hideProgress() и showData(). И тут придётся как-то очень сильно исхитриться, чтоб Presenter перестал грузить данные, т. к. они уже есть.
И такой подход с Bundle как раз используется в Mosby(ну или почти такой). И это мне в Mosby и не понравилось – нужда каждый раз руками разруливать восстановление состояния View.
Ещё, forceLain говорит, что они используют save state для хранения ViewState. Может, у них как-то по другому. Но я именно так вижу его использование =)
И ещё вопрос близкий к этому.
Я пока сам Moxy или MVP не использовал, а читаю только теоретически, но есть подозрение, что не для всего UI и не для всех экранов есть смысл использовать MVP подход. Согласны? И вот хотелось бы понять для каких?
К примеру ведь Андроид умеет сам восстанавливать View у которых прописан id, сетевой фреймворк Robospice или даже те же Loaders могут отдавать ответ асинхронной задачи в новую активити после смены конфигурации, вроде как DialogFragment умеет восстанавливать свой показ.
Т.е. встроенные средства есть и часто даже не надо писать дополнительный код.
Поэтому хочется понять какова должна быть сложность UI и его бизнес логики, чтобы стоимость использования MVP оправдала себя.
Есть ли у вас какой-то набор правил исходя из имеющегося опыта?
Может быть была бы полезная статья с живыми примерами, типа «раньше было так и такие-то проблемы», а после внедрения Moxy «стало так и проблем больше нет».
Я пока сам Moxy или MVP не использовал, а читаю только теоретически, но есть подозрение, что не для всего UI и не для всех экранов есть смысл использовать MVP подход. Согласны? И вот хотелось бы понять для каких?
К примеру ведь Андроид умеет сам восстанавливать View у которых прописан id, сетевой фреймворк Robospice или даже те же Loaders могут отдавать ответ асинхронной задачи в новую активити после смены конфигурации, вроде как DialogFragment умеет восстанавливать свой показ.
Т.е. встроенные средства есть и часто даже не надо писать дополнительный код.
Поэтому хочется понять какова должна быть сложность UI и его бизнес логики, чтобы стоимость использования MVP оправдала себя.
Есть ли у вас какой-то набор правил исходя из имеющегося опыта?
Может быть была бы полезная статья с живыми примерами, типа «раньше было так и такие-то проблемы», а после внедрения Moxy «стало так и проблем больше нет».
За время использования MVP/Moxy убедился, что MVP нужно использовать тогда, когда на вашем экране есть логика. В таком случае, выделив её в Presenter и Model, View становится максимально простой. И в то же время можно легко тестировать Model и Presenter.
В то же время, MVP помогает очень сочно производить изменения дизаайна, рефакторинг кода. Например, был у вас DailogFragment, а стал BottomSheetDialog, Snackbar или Toast(в зависимости от содержимого). И если интерфейс View был сделан максимально независимо от того, как выглядит View, то вы это сделает максимально быстро и просто. Или же поменялись какие-нибудь условия бизнес-логики. У вас опять же готовый интерфейс View, который не придётся менять. И это действительно так происходит, даже на маленьких проектах(2-3 месяца).
Так же, «Андроид умеет сам восстанавливать View у которых прописан id», но вот незадача – visibility он не восстановит =( И динамически добавленная View пропадёт.
Про Loaders я молчу – мы от них как раз убегали, когда создавали Moxy :D Но опыт использования Loaders тоже очень полезен. Полезно всё – AsyncTask, Loader, Robospice, Rx, MVP, Moxy =) Главное, почувствовать когда и где что лучше использовать.
MVP показал себя с лучшей стороны – чрезвычайно дешево завести интерфейс для View и Presenter для минимальной бизнес-логики. А профит очень приятен, даже при малейших изменениях.
Статья ещё будет, и наверное не одна. Но вряд ли там найдётся место тому, «как было раньше», потому что раньше было перепробовано слишком много всего :D
PS: другие библиотеки, реализующие MVP толком не пробовал – хватало детального изучения сорцов+сэмплов, чтобы в чём-нибудь, да расстроиться. Mosby понравился больше всего =)
В то же время, MVP помогает очень сочно производить изменения дизаайна, рефакторинг кода. Например, был у вас DailogFragment, а стал BottomSheetDialog, Snackbar или Toast(в зависимости от содержимого). И если интерфейс View был сделан максимально независимо от того, как выглядит View, то вы это сделает максимально быстро и просто. Или же поменялись какие-нибудь условия бизнес-логики. У вас опять же готовый интерфейс View, который не придётся менять. И это действительно так происходит, даже на маленьких проектах(2-3 месяца).
Так же, «Андроид умеет сам восстанавливать View у которых прописан id», но вот незадача – visibility он не восстановит =( И динамически добавленная View пропадёт.
Про Loaders я молчу – мы от них как раз убегали, когда создавали Moxy :D Но опыт использования Loaders тоже очень полезен. Полезно всё – AsyncTask, Loader, Robospice, Rx, MVP, Moxy =) Главное, почувствовать когда и где что лучше использовать.
MVP показал себя с лучшей стороны – чрезвычайно дешево завести интерфейс для View и Presenter для минимальной бизнес-логики. А профит очень приятен, даже при малейших изменениях.
Статья ещё будет, и наверное не одна. Но вряд ли там найдётся место тому, «как было раньше», потому что раньше было перепробовано слишком много всего :D
PS: другие библиотеки, реализующие MVP толком не пробовал – хватало детального изучения сорцов+сэмплов, чтобы в чём-нибудь, да расстроиться. Mosby понравился больше всего =)
C первого взгляда кажетя, что использование MVP на экранах с одной кнопкой избыточно. Но, в последствии, начинаешь воспринимать MVP как философию от которой не хочется отходить. Более того, использование одного подхода повсеместно делает приложение консистентным и легким для поддержки.
Основные проблемы MVP подхода уже решены
Так что смело используйте!
Основные проблемы MVP подхода уже решены
- boilerplates связи презентеров и вью решается статической кодогенерацией шаблонов
- хранение состояние в презентерах решается динамической кодогенерацией
Так что смело используйте!
А можно инжектить Presenter в View в поле типа не конкретного Presenter'а, а интерфейса, который данный презентер реализует?
Вот пример:
Вот пример:
Интерфейсы и т.д.
Интерфейс для презентера:
Реализация презентера:
Инжекция презентера в View
Но я бы хотел что бы тип презентера был интерфейсом, который мой презентер реализует:
На что я получаю ошибку:
Интерфейс для View надо наследовать от MvpView, а для интерфейсов для презентера никакого MvpPresenter не существует?
public interface JokePresenter {
void postJoke(String jokeText);
void getRundomJoke();
void setJoke(String jokeText);
}
Реализация презентера:
@InjectViewState
public class JokePresenterImpl extends MvpPresenter<JokeView> implements JokePresenter{
...
}
Инжекция презентера в View
public class JokeActivity extends MvpAppCompatActivity implements JokeView {
@InjectPresenter
JokePresenterImpl myJokePresenter;
...
}
Но я бы хотел что бы тип презентера был интерфейсом, который мой презентер реализует:
@InjectPresenter
JokePresenter myJokePresenter;
На что я получаю ошибку:
Error:(22, 19) error: You can not use @InjectPresenter in classes that are not View, which is typified target Presenter
Интерфейс для View надо наследовать от MvpView, а для интерфейсов для презентера никакого MvpPresenter не существует?
На данный момент такой возможности нет. И пока я не представляю, как сделать такую возможность, чтобы одновременно это было и удобно, и безопасно. Потому что в таком случае вам обязательно придётся делать provide-метод, а это может быть не очевидно, или ещё чем-нибудь не красиво.
Можно подумать на досуге, как можно сделать =)
Можно подумать на досуге, как можно сделать =)
Спасибо, я думал что чего-то не нашёл в фреймворке.
Надо же ведь стараться проектировать через интерфейсы а не через конкретную реализацию. Так и для тестирования в будущем если потребуется подставлять другие презентеры — интерфейс и потребуется.
Надо же ведь стараться проектировать через интерфейсы а не через конкретную реализацию. Так и для тестирования в будущем если потребуется подставлять другие презентеры — интерфейс и потребуется.
А как вам поможет интерфейс презентера в тестировании? Протестировать вьюху? Но смысл MVP в том, чтобы вынести всю логику в презентер. Подставлять другие презентеры во вью тоже сомнительная идея. Другой презентер — другая вью — другой интерфейс у вью.
senneco можем сделать дополнительный слой абстракции:
В каждой вью нужно будет подключаться к PresenterStore через метод MvpFacade.getPresenterStore
Дефолтная map Presenter с PresenterImpl будет генериться статической кодогенерацией и подаваться как параметр к PresenterStore
MvpFacade будет инициализировать либо тестовым PresenterStore, либо живым с дефолтной мапой.
Остался вопрос, есть ли в этом потребность и на сколько станет все сложнее для понимания)
В каждой вью нужно будет подключаться к PresenterStore через метод MvpFacade.getPresenterStore
Дефолтная map Presenter с PresenterImpl будет генериться статической кодогенерацией и подаваться как параметр к PresenterStore
MvpFacade будет инициализировать либо тестовым PresenterStore, либо живым с дефолтной мапой.
Остался вопрос, есть ли в этом потребность и на сколько станет все сложнее для понимания)
Я не могу понять, что ты предлагаешь, и зачем?) Для управления инжекцией презентера? Как-то очень сложно подход выглядит =)
Можно было не делать такие костыли с повтором команд для View, а просто для каждого View определить модельный класс, в котором будут храниться все данные нужные для отображения, и делать во View не много методов для установки данных, а один для установки модели, тогда при пересоздании View достаточно установить сохраненную модель, а не повторять все действия.
Например для экрана авторизации в таком модельном классе могли бы быть поля — логин, пароль, флаг для какого-нибудь чекбокса. При создании View, Presenter передает во View модельный класс со значениями по умолчанию если модель еще не была создана, или уже существующую.
Например для экрана авторизации в таком модельном классе могли бы быть поля — логин, пароль, флаг для какого-нибудь чекбокса. При создании View, Presenter передает во View модельный класс со значениями по умолчанию если модель еще не была создана, или уже существующую.
+ использовать Data Binding, для автоматического изменения модели при вводе текста пользователем и других действиях
Не обязательно называть это MVVM, можно использовать и ViewModel и Presenter вместе. Даже в доке гугла про Data Binding есть упоминания про Presenter https://developer.android.com/topic/libraries/data-binding/index.html
Ну, можно назвать вообще как угодно =) И да, MVP можно использовать с Data Binding, при желании. Даже есть желание попробовать такой подход. В таком случае можно не использовать аннотацию
@InjectViewState
, а просто в момент аттача передавать биндинг-объект во вью, и всё. Для этого достаточно заоверрайдить метод attachView
, и в нём передавать во вью свой объект.вы называете "костылем" особенность реализации. Это как назвать "костылем" аккумуляторы у Теслы, исправляющие баг с невозможностью ездить на бензине.
Sign up to leave a comment.
Moxy — реализация MVP под Android с щепоткой магии