Почему я не использую SharedViewModel для фрагментов?

  • Tutorial
Хабр, привет!

Задача организации взаимодействия между фрагментами встречается очень часто. На первый взгляд, ShareViewModel отлично подходит для этого. Мы создаем ViewModel с owner = наша activity, в которой отображаются наши фрагменты, и получаем эту ViewModel внутри каждого фрагмента. Т.к. владелец ViewModel — активити, то фрагменты получают один и тот же экземпляр ViewModel, что и позволяет им обмениваться данными, вызывать методы и т.д. Вот ссылка из документации.

На рисунке ниже представлена схема взаимодействия 3-х фрагментов.

image

Т.е. что мы делаем: в каждом фрагменте мы достаем SharedViewModel тех фрагментов, с которыми нам нужно взаимодействовать…

И это не самое лучшее решение, на мой взгляд. Потому что:

  1. Это полностью разрушает паттерн *VM. Мы строим ViewModel для конкретной View. ViewModel содержит разные LiveData, нужные этой View, необходимые методы и т.д. ViewModel не должна использоваться где-то ещё, кроме этой View.
  2. Каждая такая SharedViewModel — это лишняя зависимость для фрагмента. Нам нужно понимать, что в этой SharedViewModel происходит, что там нужно вызвать, чтобы второй фрагмент правильно отработал. И мы попросту можем что-то сломать.
  3. Нам нужно добавить во фрагмент все необходимые SharedViewModel, на которые этот фрагмент влияет или от которых зависит. И если в будущем появится еще фрагмент, с которым тоже надо взаимодействовать, то придется добавить в каждый фрагмент новую связь.

SharedViewModel можно использовать только тогда, когда есть несколько полностью одинаковых фрагментов с одинаковыми данными. В моей практике такого сценария еще не было. Может быть два одинаковых фрагмента, но состояние ViewModel каждого отличается, SharedViewModel для них недопустимо.

SharedViewModel приносит кучу проблем. Начинаешь путаться, все обрастает зависимостями, код усложняется. Гораздо проще работать, когда Fragment+ViewModel — это черный ящик.

Какое решение можно предложить?

Простой сервис, с которым каждый фрагмент взаимодействует по-своему.

image

Этот сервис должен передаваться внутрь каждой ViewModel. Благодаря DI и scope (Dagger, Koin, любые другие) это можно сделать легко и просто. Если сервис должен жить в контексте Activity, создайте его в Scope этой Activity и передайте каждой ViewModel.

Смотрите, что произошло: мы убрали все SharedViewModel из фрагментов. Каждый фрагмент теперь ничего не знает о других фрагментах. Если мы захотим добавить новый фрагмент в эту схему, то нам надо будет только связать viewModel D с нашим сервисом и все. Если фрагмент должен реагировать на изменение некоторого свойства сервиса — просто подписываемся на него. Нам больше не нужно разбираться в чужих ViewModels. И этот сервис будет гораздо проще любой ViewModel, разобраться в сервисе будет легче.

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

Бонус: как я создаю фрагменты?


Вот так: SomeFragment()

А что с параметрами?

С появлением ViewModel я не использую параметры. Я ничего не передаю во фрагмент через bundle (Android Studio до сих пор предлагает создавать фрагмент с аргументами и создавать фрагмент через static метод), я не использую Safety NavArgs для Navigation Component.

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

Я это все убрал во ViewModel. Я использую такой же сервис, через который передаю параметры. Он работает наподобие Safety NavArgs. ViewModel запрашивает параметры у сервиса по типу класса параметров. После успешного извлечения параметра, сервис их удаляет (работает как Push/Pop).

Это сразу решает проблему смены конфигурации: фрагмент создался снова, а ViewModel уже с актуальными данными, все что нужно, это связать их со View.

Есть также проблема при передачи параметров через bundle — это превышение допустимого объема. Если объем передаваемых данных свыше 500 кб., то приложение может аварийно завершиться из-за возникшего исключения внутри Binder.java. Это привело к созданию разных библиотек, которые передают параметры через SD, что не очень быстро по сравнению с использованием RAM.

В общем, инструменты классные, все гибко, но нужно быть осторожным.

На этом все, спасибо за внимание!

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

    0

    Я бы не стал говорить, что всё прям ужасно. Если у вас на одном экране пара фрагментов и нужно взаимодействие, и именно эта комбинация этих фрагментов только один раз встречается в приложении, то зачем придумывать себе проблему и потом же решать её? Я понимаю, что это может ухудшать переиспользуемость, но что если она и не нужна в конкретном месте..

      0
      Поправил, спасибо. Погорячился с формулировкой. Последний раз приходилось разбираться в такой мешанине из shared view model…
      –1
      а чем не устраивает вариант типа EventBus?
        0
        EventBus желательно использовать только если все приложение имеет какое то состояние и все приложение может его слушать и при этом лучше подписываться на бродкасты приложения.
        Размазывать синглтон типа отто по всему приложению, потом оттираться замучаетесь.

        Для 2 отдельных компонентов то сервис с подпиской и лучше, это их личная зависимость.
        Каждый знает, что вот зависит от такого сервиса, вполне тестируемо, подменяемо.
          0
          EventBus слишком общее решение, которое добавляет только неявных зависимостей в код. Есть печальный опыт работы с ним, когда событий действительно много. А свой сервис позволит реализовать определенную логику взаимодействия, а не только отправлять сообщения.
          +1
          Согласен с тем, что Shared ViewModel — не идеальное решение. Всё взаимодействие на нем завязывать не стоит. Но оно вполне применимо, например, в каких-нибудь пошаговых визардах. Это скорее полезный инструмент для определенного круга ситуаций.

          Но и предложенный Вами сервис Shared ViewModel полностью не заменяет. Это уже отдаленно напоминает redux или mvi.
          Остальное тоже не идеально. Опишу подробнее:

          Мы строим ViewModel для конкретной View.
          Не обязательно. В идеале ViewModel не должна знать о View, которая ее использует. Так мы разделяем логику (ViewModel) и механику (View) работы презентационного слоя. Таким образом, можно переиспользовать ViewModel на нескольких экранах схожего назначения. Например, можно использовать один и тот же тип ViewModel как в мобильной версии, так и в версии для Android TV.

          Мы не можем просто взять такой фрагмент и добавить в ViewPager, например
          И не нужно. Этим и распределением параметров должен заниматься наследник FragmentStateAdapter (или FragmentStatePagerAdapter). В идеале, по мере пролистывания, фрагменты должны создаваться и уничтожаться чтобы не мусорить в памяти. Решение этого вопроса сложнее, чем «просто добавить», и аргументы вполне подходят как часть решения.

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

          ViewModel запрашивает параметры у сервиса по типу класса параметров.
          И это порождает проблемы с уникальностью. Если у Вас в стэке несколько экземпляров одного и того-же фрагмента или фрагментов, которые используют один и тот-же тип параметров — после восстановления стейта Вы можете получить стопку одинаковых экранов.
          Пример — экран профиля пользователя с параметром userId. Вы смотрите на профиль 3, в который попали из друзей профиля 2, в который попали так же из профиля 1. И в любой момент хотите вернуться к предыдущему. Да, в случае сервиса это решаемо, но в аргументах это всё уже решено.

          Если объем передаваемых данных свыше 500 кб., то приложение может аварийно завершиться
          Потому не стоит хранить в аргументах и saved state контент. Здесь надо хранить только параметры, и, в этом случае, лимит исчерпать довольно сложно.

          Простите за большое количество текста. В паре строк эти вещи не опишешь.
            0
            Простите за большое количество текста.

            Отличная дискуссия, спасибо!

            Не обязательно. В идеале ViewModel не должна знать о View, которая ее использует. Так мы разделяем логику (ViewModel) и механику (View) работы презентационного слоя. Таким образом, можно переиспользовать ViewModel на нескольких экранах схожего назначения. Например, можно использовать один и тот же тип ViewModel как в мобильной версии, так и в версии для Android TV.


            ViewModel ничего и не знает о View. ViewModel лишь содержит ниточки, через которые она взаимодействует с View. Механика View может быть любой. У нас в проекте ViewModel отлично работает в мобильной версии и АТВ. В некоторых случаях реализация сервисов общая, в некоторых — у каждого своя (DI).

            И не нужно. Этим и распределением параметров должен заниматься наследник FragmentStateAdapter (или FragmentStatePagerAdapter).


            Предпочитаю логику отделять от Android. Задача FragmentStatePagerAdapter лишь дать нужный фрагмент по требованию пользователя. А что этот фрагмент отобразит, известно логике.

            И это порождает проблемы с уникальностью.


            Такие же проблемы могут быть и у Safty args. После восстановления state параметры и не нужны, все уже живет во ViewModel. А если есть цепочки, как с примером о профилях, то эту breadcrumb логику я бы тоже перенес в сервис. Это немного сложнее, конечно.

            Потому не стоит хранить в аргументах и saved state контент.

            Безусловно! Но бывает соблазн быстренько что-то показать, например, в диалоге через Navigation Component.

            В качестве резюме могу сказать, что я пытаюсь максимально абстрагироваться от Android, делая акцент на логике. Это помогает в тестировании и переходу к новым инструментам. Например, переход на Navigation Component получился почти безболезненным. Возможно, это также поможет мне, когда мы захотим попробовать Kotlin multiplatform.
              +1

              Добрый день, почти получилось, только смерть процесса приложения забили учитывать.

                0
                Предпочитаю логику отделять от Android.
                В данном конкретном случае адаптер, скорее, часть механики View. А вот дочерние фрагменты уже подключены к логике. В этом кейсе как раз и выгодно использовать Shared ViewModel — родительский фрагмент ее порождает, дочерние (из ViewPager) подключаются. При этом ничто Вам не мешает в дочерних порождать собственные ViewModel для, например, поставки данных.
                Так или иначе, всю логику за пределы Android Вы не сможете вынести, как бы Вам не хотелось.
                Такие же проблемы могут быть и у Safty args
                Не могу представить себе такого кейса. Safe Args — просто обертка над аргументами. А один инстанс аргументов приходится на один инстанс фрагмента.
                то эту breadcrumb логику я бы тоже перенес в сервис
                Как говорится, «у любой задачи есть минимум 3 решения». Можно и по-Вашему, а можно взять готовое решение — Navigation Component + Safe Args, что обойдется дешевле во всех смыслах.
                что я пытаюсь максимально абстрагироваться от Android, делая акцент на логике. Это помогает в тестировании и переходу к новым инструментам.
                Ну вот для этого пакеты AndroidX поставляются с зависимостями для тестирования. Navigation Component не исключение.
              0
              А что будет с параметрами которые вьюмодель достает из стека если она будет пересоздана после смерти процесса? Или тут начинается возня с сохранением состояния?
                0
                С процессами всегда возни много. В момент сохранения состояния сервис параметров сбрасывает свои данные на persistance storage, потом достает, если надо. Здесь, конечно, простой push/pop не подойдет.
                  +1

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

                    0
                    Что думаете насчет нового способа передачи данных между фрагментами FragmentResultOwner?
                    Решит ли это проблему?
                      0
                      Ну как бы взаимодействие между фрагментами — это не только отправка аргументов и получение результата. Часто нужно и во время работы фрагмента что-то дергать или получать в других фрагментах.
                        0

                        Такую штуку и не видел даже, в альфе ещё, вроде. Функционала недостаточно пока ещё, можно только передать результат, чем-то напоминает onActivityResult. Вот интересно, зачем в Гугл такую штуку добавили, ведь здесь тоже можно использовать SharedViewModel… Возможно, там тоже понимают проблему, и вскоре мы увидим новый arch component. Хорошо бы...

                          0
                          Возврат результата из фрагмента часто используется, например, в DialogFragment'ах. Они могут быть вызваны из разных экранов и поэтому не могут быть привязаны к какой-то одной shared ViewModel'и. Приходилось изворачиваться с getActivity/getParentFragment + кастинг к интерфейсу. А с Navigation Component и это не прокатывает, приходилось использовать костыли с nav back stack.
                        0

                        Несколько категоричное мнение, если честно. Фрагменты — это необязательно разные экраны в приложении. Если несколько фрагментов не представляют собой автономные единицы, то вполне можно сделать одну ViewModel. Да и сама она не должна знать о том, один там фрагмент или несколько. Это особенности отображения, которые еще и могут отличаться от устройства к устройству, в зависимости от конфигурации.

                          0
                          интересно было бы посмотреть на код
                            0
                            В свободное время я работаю над простым framework'ом на основе своих идей. Хороший пример будет.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое