Pull to refresh

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 отписываются через делегат и по этому нигде ничего не течет :)
У вас видимо ViewState хранится во View, поэтому вы вынуждены сериализовывать его и складывать в Bundle?

Мы решили развязать пользователю руки, и поэтому ссылка на 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 и вешать им разные презентеры. Это на примерно такой случай: открыть активити «чатик с другом», из неё открыть активити «список друзей друга», а из неё открыть еще одну активити «чатик с еще одним другом». В итоге получим первую и последнюю активити одного класса, но чатик там должен быть разный, соответственно, и презентеры тоже разные.
Понятно, а мы решили, что раз процесс убился, и всё-равно потерялись все Presenter, то просто пусть заново будет создан Presenter и всё начнётся сначала. Я замечал, что у стоковых Android-приложений именно такое поведение =)

Да, у нас тоже легко сделать кейс что на другой активити такого же типа будет использоваться другой Presenter =) Вообще, изначально все Presenter – локальные. И, соответственно, на каждый экран свои Presenter. А вот если указать глобальный тэг, то будет использоваться везде один Presenter. Ну и спец. фишка – динамический тэг для глобального презентера. Например, открыли список своих контакто☘ → создался Presenter для нашего списка контактов. Затем открыли список контактов друга → создался Presenter для списка его контактов. Затем вернулись к своему списку контактов, и тут уже не создаётся новый Presenter, а берётся старый. Актуально может быть, например, если эти Presenter очень долго отрабатывают и будет обидно потерять их.

А ваше решение где-нибудь опубликовано? Было бы интересно посмотреть =)
Интересно, что вы еще написали себе аннотации. Лично мне не очень хотелось писать собственные кодогенерирующие аннотации для инжекта, на крайний случай хватает инжектов из даггера, но раз у вас есть, наверно стоит взглянуть на них тоже.

А ваше решение где-нибудь опубликовано?


Ага, на внутрикомпанейском гитлабе :) Вероятно, когда нибудь оформим и в общий доступ
Да, мы очень хотели, чтоб пришлось писать минимум кода. И в то же время хотелось попробовать annotation processor =) Результат крайне порадовал – для полноценного сохранения состояния достаточно применить аннотацию @GenerateViewState к MvpView и @InjectViewState к MvpPresenter. Когда видишь этот код и результат его работы, кажется что там есть магия =)

Правда, если можно обойтись без кодогенерации/рефлексии, используя только наследование/композицию, это наверное даже круче.
Вот как это можно сделать в moxy:
  • в каждом методе 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.

Предположим:

  • есть 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 был свой Presenter. Так вам будет проще всего – не нужно будет ничего разруливать.

В таком случае, каждый Fragment будет по-своему инициализировать свой Presenter, а Presenter будет уже доставать нужные данные. И вам будет очень просто обработать команды из Presenter, и оба фрагмента будут независимы друг от друга.
А вот предположим, мы хотим создать в своем коде отдельную сущность Router, тот самый, из соседней статьи про VIPER, и вызывать его методы из презентеров. Естественно, хочется обойтись без бойлерплейта, но не очень понятно как, роутеру для запуска активити нужен контекст — текущая активити.
Простое и некрасивое решение — явно вызывать Presenter.start(this) в OnCreate/OnStart. А есть ли решение красивее?
Главный вопрос, который нам здесь нужно решить – а где будет жить экземпляр Router?

Если он будет жить и использоваться только во View, то никаких проблем нет – у нас есть напрямую доступ к Activity.

Сложней, если ссылка на Router нужна внутри Presenter. В таком случае вам не обойтись без явной передачи "чего-то" из View в Presenter. Здесь вы встаёте перед другим выбором: что передать из View? Context? А если Router будет не стартовать Activity, а менять фрагменты? Тогда придётся передавать что-то другое. Таким образом само собой напрашивается решение из соседней статьи: из View вы устанавливаете в Presenter непосредственно экземпляр Router, с которым в будущем будете работать из Presenter.

Мне кажется, если реализовывать VIPER, то нужно идти по второму пути и просто в onCreate передавать в Presenter экземпляр Router. В таком случае хотелось бы обратить внимание на две вещи:

  1. Не забудьте убирать Router из Presenter, когда View уничтожается(иначе будет утечка памяти)
  2. Вы можете расширить функционал 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 не освобождает память
Интересное замечание. Действительно, состояние не будет восстановлено, т.к. у фрагмента не будет вызван метод onCreate(). Значит, в случае с retain-фрагментом можно поступить например так: вызывать метод делегата onCreate() не в onCreate() фрагмента, а где-нибудь в другом месте. Например, в методе onCreateView(). Правда, метод onAttach() может быть более подходящим местом, но я с ходу не могу ручаться за вызовы этого метода у retain-фрагментов.

А если делать не retain-фрагмент, то утечки памяти не должно быть – при вызове метода onDestroy() у фрагмента, он будет отвязан от презентера. В то же время в презентере хранятся weak references на View, поэтому утечки не должно быть. Может быть вы как-то самостоятельно храните ссылку на фрагмент где-нибудь в презентере?
Еще небольшой вопрос:

При загрузке данных — показываем диалоговое окно с прогрессом ( progress)
Что-то пошло не так, скрываем progress и показываем сообщение об ошибке ( error )
Пользователь прочитал сообщение об ошибке и закрыл error.

Так как это все делается через presenter, все сохраняется во view state
И при перевороте экрана отработают все 4 метода:

  1. Показать progress
  2. Скрыть progress
  3. Показать error
  4. Скрыть error

Отработает целых 4 метода ( при перевороте ), но по сути они отработают впустую, т.к. пользователь в такой ситуации не должен ничего увидеть.

Вопрос:
Можно и нужно ли как-то очищать очередь во view state?
Если будет вызвано гораздо больше методов, не приведет ли это к ненужной трате ресурсов?
Для этого предусмотрены стратегии, применяются либо на весь view-интерфейс, либо на отдельные методы, можно писать свои. Подробнее в посте и исходниках.
Эти команды применятся очень быстро, что пользователь не почувствует, что применилось несколько команд.

Но в случае, если вы хотите, вы можете написать свою стратегию, которая будет удалять команду, к которой текущая является противодействием. Или же, если новая команда приводит View в такое состояние, что все предыдущие команды становятся точно не нужными, то можно применять стратегию SingleStateStrategy. Например если есть команда showData, и нет swipe to refresh, то можно к ней применить эту стратегию, т.к. после того, как установили данные, точно не нужно ни ошибку показывать, ни прогресс.
Да, именно так. Когда вы применяете аннотацию @InjectViewState, annotation processor понимает, ViewState какой View вы хотите использовать, и генерирует его, если его ещё нет.
День добрый.
Очень интересное поведение наблюдал на 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)
Может не внимательно прочитал, но не очень понял есть ли принципиальное отличие 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. Может, у них как-то по другому. Но я именно так вижу его использование =)
И ещё вопрос близкий к этому.
Я пока сам 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 понравился больше всего =)
C первого взгляда кажетя, что использование MVP на экранах с одной кнопкой избыточно. Но, в последствии, начинаешь воспринимать MVP как философию от которой не хочется отходить. Более того, использование одного подхода повсеместно делает приложение консистентным и легким для поддержки.

Основные проблемы MVP подхода уже решены
  • boilerplates связи презентеров и вью решается статической кодогенерацией шаблонов
  • хранение состояние в презентерах решается динамической кодогенерацией

Так что смело используйте!
А можно инжектить Presenter в View в поле типа не конкретного Presenter'а, а интерфейса, который данный презентер реализует?
Вот пример:
Интерфейсы и т.д.
Интерфейс для презентера:
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, либо живым с дефолтной мапой.

Остался вопрос, есть ли в этом потребность и на сколько станет все сложнее для понимания)

В MVP не предполагается использование разных презентеров для одной вью. Вью имеет доступ к конкретному презентеру. Зачем там интерфейс? Какие публичные методы он будет скрывать? Для кого?
Мокси реализует чистый архитектурный подход. Не надо ломать принципы
Я не могу понять, что ты предлагаешь, и зачем?) Для управления инжекцией презентера? Как-то очень сложно подход выглядит =)
Да, излишне запутанно. Это тот самый случай когда надо выбирать простоту
Можно было не делать такие костыли с повтором команд для View, а просто для каждого View определить модельный класс, в котором будут храниться все данные нужные для отображения, и делать во View не много методов для установки данных, а один для установки модели, тогда при пересоздании View достаточно установить сохраненную модель, а не повторять все действия.

Например для экрана авторизации в таком модельном классе могли бы быть поля — логин, пароль, флаг для какого-нибудь чекбокса. При создании View, Presenter передает во View модельный класс со значениями по умолчанию если модель еще не была создана, или уже существующую.
+ использовать Data Binding, для автоматического изменения модели при вводе текста пользователем и других действиях
Конечно можно. Просто это тогда называется MVVM. Но мне больше по душе MVP. Спорить о том, что лучше — не вижу смысла ;) Тем более про это есть целая статья, и её писал человек, который использовал и тот, и тот подход.
Не обязательно называть это 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.

Articles

Change theme settings