Ads
Comments 46
+1
Очень хороший подход! Очень забавно, что мы, создавая свою 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 отписываются через делегат и по этому нигде ничего не течет :)
0
У вас видимо ViewState хранится во View, поэтому вы вынуждены сериализовывать его и складывать в Bundle?

Мы решили развязать пользователю руки, и поэтому ссылка на ViewState хранится в Presenter. Presenter в свою очередь хранится не в Activity, а в статичном хранилище. Это позволяет не зависеть Presenter(а значит и ViewState) от жизненного цикла View. И поэтому даже если команда во ViewState прилетела в то время, когда View не приаттачена к Presenter/к ViewState, как только View будет приаттачена, ViewState сообщит ей весь набор команд, которые она должна выполнить. За счёт этого можно из Presenter передавать в командах даже несериализуемые данные.

А если вы это и говорили, то круто, что мы не одни так подумали =)

И да, у нас идёт тэгирование не View, а Presenter ;)
0
Да, если вдаваться в детали, мы положили ViewState в активити для того, что бы иметь возможность «вернуть все как было» не только при повороте экрана, но и при пересоздании всего процесса приложения (весьма частый кейс в Android 6 со своими новыми runtime пермишеннами). Конечно после таких издевательств над процессом в нем не останется никаких презентеров в статичном хранилище, а вот весь ActivityTask андроид нам любезно восстанавливает и отдает savedState, а там наша ViewState лежит себе :)
А для «легкого» пересоздания View, как в случае с поворотом, мы как раз так же используем хранилище презентеров. Вот только оно не статичное, а создается и лежит внутри Application. А тегирование для View мы используем не только для того, что бы автоматически переаттачить тот же презентер к новой View, но и для того, что бы разделять одинаковые Activity в одном AcivityTask и вешать им разные презентеры. Это на примерно такой случай: открыть активити «чатик с другом», из неё открыть активити «список друзей друга», а из неё открыть еще одну активити «чатик с еще одним другом». В итоге получим первую и последнюю активити одного класса, но чатик там должен быть разный, соответственно, и презентеры тоже разные.
0
Понятно, а мы решили, что раз процесс убился, и всё-равно потерялись все Presenter, то просто пусть заново будет создан Presenter и всё начнётся сначала. Я замечал, что у стоковых Android-приложений именно такое поведение =)

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

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

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


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

Правда, если можно обойтись без кодогенерации/рефлексии, используя только наследование/композицию, это наверное даже круче.
0
Вот как это можно сделать в moxy:
  • в каждом методе View сохранять в Bundle какое-то описание состояния
  • складывать этот Bundle в outState
  • в onCreate передавать этот Bundle в Presenter
  • в Presenter смотреть в метод onFistViewAttached, есть ли Bundle
  • если есть Bundle, «парсить» его и давать команды во ViewState


У этого способа есть минус – он не автоматизирован. Но есть и плюс – лишний раз Bundle парситься не будет. А вы как-нибудь автоматизировали создание сериализуемого ViewState?
0
У нас View только отражает состояние ViewState и передает клики и т.п. презентеру. ViewState изменяясь сообщает об этом View и View уже показывает прогрессы или пезультаты или еще чего. В onSaveInstanceState базоый презентер серелизует ViewState в бандл стандартным ObjectOutputStream и потом достает из бандла в onCreate() стандартным же ObjectInputStream. Этого хватает для большинства экранов. Если на каком то экране во ViewState требуется положить что-то такое, чего не стоит серелизовать этими средствами, то можно переопределить серелизацию/десерелизацию конкретно для этого экрана и пары ViewState-Presenter.
0
Подскажите, пожалуйста, как правильно передать данные из 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), а нужно, только один раз.
0
Если вам нужно, чтоб команда отрабатывала исключительно один раз, значит она не должна быть сохранена во ViewState. Для этого у неё должна быть стратегия SkipStrategy. Её можно указать, применив к методу start в интерфейсе View аннотацию: @StateStrategyType(SkipStrategy.class)

Ещё на заметку, ваш код можно изменить:

  • в activity, в методе onCreate выполните ваш код presenter.setStartValue(getIntent().getExtras().getInt(Constants.VALUE));
  • в presenter, в методе setStartValue сохарните пришедшее значение где-нибудь в presenter
  • в методе onFirstViewAttach берёте это значение и работаете с ним

Но это не обязательно – ваш подход абсолютно так же будет работать. Просто имейте ввиду возможность такого способа =)

Учтите, что метод onFirstViewAttach будет вызван только при первом привязывании view. А после поворота девайса, он уже не будет вызван. Но похоже вы это и так поняли =)
0
Как правильно поступить в такой ситуации:

Есть ViewPager в котором находятся 2 фрагмента (Fragment 1, Fragment 2). UI и логика фрагментов идентичны, отличие только в выборке данных из БД.

Вопрос: Можно как-то при переходе с одного фрагмента на другой обнулять/сбрасывать ViewState. Т.е. если на Fragment 1 было показано диалоговое окно и которое должно быть показано при перевороте устройства, то при переходе на другой фрагмент, Fragment 2 должен быть в первоночальном состоянии.

В такой ситуации нужно делать один presenter и при переходе между фрагментами обрабатывать события. Или лучше сделать 2 разных presenters, но тогда получим дублирование кода.
0
В большинстве случаев, здесь будет достаточно сделать так, чтоб на каждую страницу ViewPager был свой Presenter. Так вам будет проще всего – не нужно будет ничего разруливать.

В таком случае, каждый Fragment будет по-своему инициализировать свой Presenter, а Presenter будет уже доставать нужные данные. И вам будет очень просто обработать команды из Presenter, и оба фрагмента будут независимы друг от друга.
0
А вот предположим, мы хотим создать в своем коде отдельную сущность Router, тот самый, из соседней статьи про VIPER, и вызывать его методы из презентеров. Естественно, хочется обойтись без бойлерплейта, но не очень понятно как, роутеру для запуска активити нужен контекст — текущая активити.
Простое и некрасивое решение — явно вызывать Presenter.start(this) в OnCreate/OnStart. А есть ли решение красивее?
0
Главный вопрос, который нам здесь нужно решить – а где будет жить экземпляр 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. Тогда вы не потеряете ваши изменения после изменения конфигурации.
0
Спасибо, вы мне помогли упорядочить мысли на тему навигации.
0
Подскажите пожалуйста.
Если использовать Presenter для Fragment и во Fragment установить setRetainInstance(true); то при переворроте экрана, Presenter не востанавливает состояние View.

Если же убрать setRetainInstance(true); то при перевороте, происходит утечка памяти GC не освобождает память
0
Интересное замечание. Действительно, состояние не будет восстановлено, т.к. у фрагмента не будет вызван метод onCreate(). Значит, в случае с retain-фрагментом можно поступить например так: вызывать метод делегата onCreate() не в onCreate() фрагмента, а где-нибудь в другом месте. Например, в методе onCreateView(). Правда, метод onAttach() может быть более подходящим местом, но я с ходу не могу ручаться за вызовы этого метода у retain-фрагментов.

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

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

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

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

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

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

Но в случае, если вы хотите, вы можете написать свою стратегию, которая будет удалять команду, к которой текущая является противодействием. Или же, если новая команда приводит View в такое состояние, что все предыдущие команды становятся точно не нужными, то можно применять стратегию SingleStateStrategy. Например если есть команда showData, и нет swipe to refresh, то можно к ней применить эту стратегию, т.к. после того, как установили данные, точно не нужно ни ошибку показывать, ни прогресс.
0
Да, именно так. Когда вы применяете аннотацию @InjectViewState, annotation processor понимает, ViewState какой View вы хотите использовать, и генерирует его, если его ещё нет.
0
День добрый.
Очень интересное поведение наблюдал на Android 6.

Есть TestActivity и Presenter. В приложении есть permission, например на использование камеры.
TestActivity запущена, метод onFirstViewAttach() — отработал.
Затем, пользователь заходит в Settings и включает/выключает permission.
Возращается в TestActivity и метод onFirstViewAttach опять отрабатывает.

Когда включаешь/отключаешь permission getMvpDelegate().onDestroy(); не отрабатывает.

Почему onFirstViewAttach() отрабатывает?
0
Видимо, Android полностью останавливает приложение в таком случае. Тут вам не помогут даже глобальные presenter. Но похоже, что у activity должен был быть вызван метод onSaveInstanceState – в нём вы можете сохранить какие-нибудь флаги для presenter. А в onCreate передавать эти флаги в presenter(банально сделать метод init(Bundle args) в presenter), и уже в presenter решать, делать что-нибудь со view в onFirstViewAttach, или нет.
0
Переключение настроек полностью убивает процесс приложение и создает новый, по этому все презентеры тоже убиваются.
onSaveInstanceState()/onCreate() действительно вызываются, но не те, к которым мы привыкли, а новые, те что с 21 API: onSaveInstanceState(android.os.Bundle, android.os.PersistableBundle) и onCreate(android.os.Bundle, android.os.PersistableBundle)
0
Может не внимательно прочитал, но не очень понял есть ли принципиальное отличие ViewState от механизма самого Андроида(onsaveinstancestate/onRestoreState) для случая связи одной View для одного Presenter'a?
0
Я даже не знаю, есть ли ясный ответ «да» или «нет». Могу только рассказать в чём разница, а вы уже сами определитесь =)

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. Может, у них как-то по другому. Но я именно так вижу его использование =)
0
И ещё вопрос близкий к этому.
Я пока сам Moxy или MVP не использовал, а читаю только теоретически, но есть подозрение, что не для всего UI и не для всех экранов есть смысл использовать MVP подход. Согласны? И вот хотелось бы понять для каких?
К примеру ведь Андроид умеет сам восстанавливать View у которых прописан id, сетевой фреймворк Robospice или даже те же Loaders могут отдавать ответ асинхронной задачи в новую активити после смены конфигурации, вроде как DialogFragment умеет восстанавливать свой показ.
Т.е. встроенные средства есть и часто даже не надо писать дополнительный код.

Поэтому хочется понять какова должна быть сложность UI и его бизнес логики, чтобы стоимость использования MVP оправдала себя.
Есть ли у вас какой-то набор правил исходя из имеющегося опыта?
Может быть была бы полезная статья с живыми примерами, типа «раньше было так и такие-то проблемы», а после внедрения Moxy «стало так и проблем больше нет».
0
За время использования 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 понравился больше всего =)
0
C первого взгляда кажетя, что использование MVP на экранах с одной кнопкой избыточно. Но, в последствии, начинаешь воспринимать MVP как философию от которой не хочется отходить. Более того, использование одного подхода повсеместно делает приложение консистентным и легким для поддержки.

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

Так что смело используйте!
0
А можно инжектить 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 не существует?
0
На данный момент такой возможности нет. И пока я не представляю, как сделать такую возможность, чтобы одновременно это было и удобно, и безопасно. Потому что в таком случае вам обязательно придётся делать provide-метод, а это может быть не очевидно, или ещё чем-нибудь не красиво.

Можно подумать на досуге, как можно сделать =)
0
Спасибо, я думал что чего-то не нашёл в фреймворке.
Надо же ведь стараться проектировать через интерфейсы а не через конкретную реализацию. Так и для тестирования в будущем если потребуется подставлять другие презентеры — интерфейс и потребуется.
+1

А как вам поможет интерфейс презентера в тестировании? Протестировать вьюху? Но смысл MVP в том, чтобы вынести всю логику в презентер. Подставлять другие презентеры во вью тоже сомнительная идея. Другой презентер — другая вью — другой интерфейс у вью.

0
senneco можем сделать дополнительный слой абстракции:

В каждой вью нужно будет подключаться к PresenterStore через метод MvpFacade.getPresenterStore

Дефолтная map Presenter с PresenterImpl будет генериться статической кодогенерацией и подаваться как параметр к PresenterStore

MvpFacade будет инициализировать либо тестовым PresenterStore, либо живым с дефолтной мапой.

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

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

Например для экрана авторизации в таком модельном классе могли бы быть поля — логин, пароль, флаг для какого-нибудь чекбокса. При создании View, Presenter передает во View модельный класс со значениями по умолчанию если модель еще не была создана, или уже существующую.
0
+ использовать Data Binding, для автоматического изменения модели при вводе текста пользователем и других действиях
0
Конечно можно. Просто это тогда называется MVVM. Но мне больше по душе MVP. Спорить о том, что лучше — не вижу смысла ;) Тем более про это есть целая статья, и её писал человек, который использовал и тот, и тот подход.
0
Не обязательно называть это MVVM, можно использовать и ViewModel и Presenter вместе. Даже в доке гугла про Data Binding есть упоминания про Presenter https://developer.android.com/topic/libraries/data-binding/index.html
0
Ну, можно назвать вообще как угодно =) И да, MVP можно использовать с Data Binding, при желании. Даже есть желание попробовать такой подход. В таком случае можно не использовать аннотацию @InjectViewState, а просто в момент аттача передавать биндинг-объект во вью, и всё. Для этого достаточно заоверрайдить метод attachView, и в нём передавать во вью свой объект.
0

вы называете "костылем" особенность реализации. Это как назвать "костылем" аккумуляторы у Теслы, исправляющие баг с невозможностью ездить на бензине.

Only those users with full accounts are able to leave comments.  , please.