Оно так и должно быть. Сущности, управляющие View (presenter, controller, VM, и т.п.) не должны выходить за пределы своего модуля и app не должен о них знать. Чем меньше модули знают друг о друге - тем лучше. В целом, если что-то может быть internal, то оно и должно таким быть.
Если очень хотите сделать через фабрику с мапой из app, то тут, похоже, без каких-либо интерфейсов в общем модуле не сделать. Но решение выглядит странным imho.
ViewModel должна жить в модуле вместе со своим экраном и быть internal. Нет смысла куда-то выносить фабрики VM в другой модуль.
Создание VM будет аналогично созданию презентера в примере. Или в этом примере - созданию контроллера. Вы, возможно, захотите использовать в фрагменте "by viewModels()" с кастомной фабрикой для создания VM.
В общем, не усложняйте. Попробуйте сделать VM internal и создавать кастомной фабрикой. Пример не подскажу, к сожалению.
Насколько я понимаю, у Вас такая ситуация: есть экран А, он отображается и где-то меняет состояние. Потом погибает и отображается экран Б, который должен поюзать это состояние.
Если ссылка на компонент, где живёт это состояние, закопана только в компонентах экранов, то да, состояние может потеряться (зависит от того, как быстро GC его грохнет).
В вашем случае состояние можно выделить в отдельную фичу со своим компонент-холдером и хранить ссылку на него в какой-то сущности, которая будет жива вне зависимости от жизненного цикла экранов А и Б. Самый простой вариант - в application, тогда этот компонент будет жить пока живёт приложение.
Я бы рекомендовал ещё подумать насчёт базы на data уровне. Тогда всё просто: модифицируем базу и изменения доступны для всех фичей. Можно даже не базу, а просто сеттинги. Или даже просто в памяти хранить. Самое важное - на data уровне и закрыть имплементацию репозиторием. Я делал такой пример: https://github.com/PavelSidyakin/ProductList Там Like шарится между экранами.
Это действительно слэнг :)) Означает что-то типа "заберёт себе". В данном случае имеется в виду "будет держать у себя" ссылку. Английский аналог "hold".
Насчёт тестов с логикой. В примере выше - да, согласен, что не стоит так делать.
А что насчёт параметризованных тестов, когда параметр приходит извне - в функцию теста? Кажется, что тут нормально написать if (параметр) { ... } else { ... }. В этом случае из одной тестовой функции образуется несколько тестов - сколько значений может принять параметр.
Данную проблему можно решить с применением подхода, описанного здесь: habr.com/ru/post/536106
В данном случае, нужно чтобы у активити был свой компонент и к нему можно привязать сколько угодно других компонентов. Когда компонент активити умрёт, то и все привязанные к нему компоненты тоже освободятся (помрут, если не используются где-то ещё).
Случай с активити описан в указанной статье. Обратите внимание на особенности работы с компонентом активити.
Взять хоть нотификации. Для показа нужен контекст приложения. Что уже делает этот модуль не утилитарным. Ему нужно в зависимости предоставлять контекст, а лучше вообще интерфейс репозитория, чтобы не зависеть от андроидских классов (фича тогда будет кроссплатформенной).
Биллинг (повезло, что у вас его нет :)). Тут понятно — у модуля будет куча репозиториев для общения с бэком, магазинами и т.п. При этом фичи могут проверять, можно ли что-то показывать при данном лицензионном состоянии, можно ли запускать сервисы, отображать ли кнопки и т.п. Т.е. фича биллинга — она именно фича и от неё могут многие зависеть.
И даже модуль с набором экранов — от него тоже могут зависеть другие модули. Эти экраны-то ведь нужно запускать. И могут быть общие экраны, например, фрагмент камеры. От него могут зависеть несколько других фичей.
В моём понимании, утилитарный модуль — это модуль, которому действительно ничего не нужно. Он содержит только общие функции и у него нет зависимостей, ни на что, даже на контекст приложения.
Пусть у нас есть фича А, которая хочет воспользоваться фичёй Б.
Чтобы фича А воспользовалась фичёй Б, нужно:
1. Чтобы у фичи А был свой компонент и ComponentHolder.
2. Фича А декларирует в своих зависимостях интерфейс(ы) из фичи Б.
3. При инициализации ComponentHolder'а фичи А, мы прописываем, что она зависит от фичи Б и отдаём ей нужные интерфейсы из фичи Б.
4. Когда компонент фичи А будет создан, автоматически создастся и компонент фичи Б.
5. Внутри фичи А мы можем использовать интерфейсы фичи Б, которые были продекларированы в зависимостях. В случае dagger мы можем эти интерфейсы инжектить как любые другие интерфейсы из компонента фичи А.
Это было бы идеально, если бы все модули могли зависеть только от утилитарных модулей.
Но реально модули вполне могут зависеть от других «фичевых» модулей или core-модулей.
Например, фичевые модули, от которых, скорее всего, будет зависеть много других модулей: лицензирование (сюда же покупки, биллинг), аналитика/статистика, управление камерой, пуш-нотификации и многие другие. Они утилитарные? Нет! Хотя бы необходимость контекста уже делает модуль не утилитарным. А некоторым ещё и репозитории свои понадобятся.
Плюс есть core-модули, от которых зависит несколько фич.
В общем, такого не бывает, чтобы все модули зависели только от утилитарных.
По поводу инициализации «100500 статиков в Application.onCreate()».
Да, тут получается портянка кода инициализации. Но её можно разбить на функции и отдельные файлы. Да и нужный код в этой портянке легко находить – достаточно перейти в имплементацию интерфейса зависимостей модуля. Плюс, как упомянуто в статье, порядок инициализации не важен, так что сильно закапываться в этой портянке не придётся.
Ещё, тут не идёт инициализация модулей, т.е. внутренние компоненты модулей не создаются сразу в Application.onCreate(). Здесь идёт инициализация Component Holder’ов, т.е. инициализация способа инициализации модуля. Потом каждый модуль (точнее, внутренний компонент модуля) инициализируется лениво – когда он кому-то понадобится. И уничтожается, когда он никому не нужен.
Ок, давайте рассмотрим, что будет, если каждый модуль будет сам себя инициализировать, т.е. будет сам себя склеивать с другими модулями.
Пусть у нас есть модуль А, который зависит от интерфейсов модуля Б.
Чтобы модуль А себя проинициализировал, он должен проинициализировать модуль Б. А это значит, он будет знать о деталях имплементации модуля Б, и, что самое печальное, зависеть от модуля, который содержит имплементацию модуля Б.
Если в приложении используется подход с разделением модулей на API/Impl для ускорения сборки, то модуль А будет зависеть и от API модуля Б и от его Impl, что сводит на нет цель разбиения на API/Impl – при изменении Impl модуля Б, пересоберётся и модуль А.
Если же мы делегируем склейку кому-то другому – модулю, который знает обо всех, то получится, что модуль А знает только об API модуля Б. И изменение Impl модуля Б не приведёт к пересборке модуля А. На роль «модуля, который знает обо всех» вполне подходит модуль Application. Можно сделать ещё один модуль, который будет склеивать и тоже знать обо всех других модулях, но не очень ясен смысл делать ещё один всезнающий модуль.
В итоге этот подход с чётким выделением интерфейсов FeatureAPI, FeatureDependencies и внешней склейкой позволяет делать модули более чистыми. У модуля чёткое API и чёткие зависимости. И модулю без разницы как будут проставлены имплементации зависимостей. Модуль просто декларирует, что «мне нужно это», а задача простановки зависимостей (склейка) – это не его проблемы.
Спасибо за фидбэк!
По поводу «отключаемости комментированием в build.gradle».
Это уже похоже на архитектуру с плагинами, когда в приложение можно динамически добавлять какой-либо функционал. Это можно реализовать с помощью динамической загрузки jar-ника, через AIDL (тогда дополнительный функционал добавляется установкой ещё одной APK) или другим способом. Но такая архитектура довольно-таки сложна в реализации и создавать её нужно только если это реально нужно. В реальных проектах такое встречается редко.
В реальности, модули создаются не для быстрой отключаемости. О целях выделения в модули написано в статье Андрея, упомянутой в начале текущей статьи. Прочитайте, чтобы понять для чего это вообще нужно.
Что использовать — MVP или MVVM — дело вкуса и поставленной цели.
Хоть внутри MVP c Moxy и напоминает MVVM, но снаружи, т.е. для пользователя библиотеки, это чистый MVP.
Отсюда и все преимущества MVP — более тщательное покрытие unit-тестами поведения UI. С MVVM можно протестировать модель, но не как она используется в имплементации.
Например, в случае MVVM, в модели может быть метод типа fun getData(): Data. Мы можем протестировать unit-тестом что вернул этот метод, но не то, как его использует View.
В случае с MVP в presenter не может быть get-методов. View очень глупая и только presenter говорит View что делать, а значит можно тщательнее протестировать поведение UI unit-тестом presenter'а.
Так что если покрытие тестами — не важно, то можно использовать MVVM. Если важно — то лучше MVP.
Оно так и должно быть. Сущности, управляющие View (presenter, controller, VM, и т.п.) не должны выходить за пределы своего модуля и app не должен о них знать. Чем меньше модули знают друг о друге - тем лучше. В целом, если что-то может быть internal, то оно и должно таким быть.
Если очень хотите сделать через фабрику с мапой из app, то тут, похоже, без каких-либо интерфейсов в общем модуле не сделать. Но решение выглядит странным imho.
Здравствуйте!
ViewModel должна жить в модуле вместе со своим экраном и быть internal. Нет смысла куда-то выносить фабрики VM в другой модуль.
Создание VM будет аналогично созданию презентера в примере. Или в этом примере - созданию контроллера. Вы, возможно, захотите использовать в фрагменте "by viewModels()" с кастомной фабрикой для создания VM.
В общем, не усложняйте. Попробуйте сделать VM internal и создавать кастомной фабрикой. Пример не подскажу, к сожалению.
Доброе утро :)
Насколько я понимаю, у Вас такая ситуация: есть экран А, он отображается и где-то меняет состояние. Потом погибает и отображается экран Б, который должен поюзать это состояние.
Если ссылка на компонент, где живёт это состояние, закопана только в компонентах экранов, то да, состояние может потеряться (зависит от того, как быстро GC его грохнет).
В вашем случае состояние можно выделить в отдельную фичу со своим компонент-холдером и хранить ссылку на него в какой-то сущности, которая будет жива вне зависимости от жизненного цикла экранов А и Б. Самый простой вариант - в application, тогда этот компонент будет жить пока живёт приложение.
Я бы рекомендовал ещё подумать насчёт базы на data уровне. Тогда всё просто: модифицируем базу и изменения доступны для всех фичей. Можно даже не базу, а просто сеттинги. Или даже просто в памяти хранить. Самое важное - на data уровне и закрыть имплементацию репозиторием. Я делал такой пример: https://github.com/PavelSidyakin/ProductList Там Like шарится между экранами.
Это действительно слэнг :)) Означает что-то типа "заберёт себе". В данном случае имеется в виду "будет держать у себя" ссылку. Английский аналог "hold".
Насчёт тестов с логикой. В примере выше - да, согласен, что не стоит так делать.
А что насчёт параметризованных тестов, когда параметр приходит извне - в функцию теста? Кажется, что тут нормально написать if (параметр) { ... } else { ... }. В этом случае из одной тестовой функции образуется несколько тестов - сколько значений может принять параметр.
В данном случае, нужно чтобы у активити был свой компонент и к нему можно привязать сколько угодно других компонентов. Когда компонент активити умрёт, то и все привязанные к нему компоненты тоже освободятся (помрут, если не используются где-то ещё).
Случай с активити описан в указанной статье. Обратите внимание на особенности работы с компонентом активити.
habr.com/ru/post/536106
Взять хоть нотификации. Для показа нужен контекст приложения. Что уже делает этот модуль не утилитарным. Ему нужно в зависимости предоставлять контекст, а лучше вообще интерфейс репозитория, чтобы не зависеть от андроидских классов (фича тогда будет кроссплатформенной).
Биллинг (повезло, что у вас его нет :)). Тут понятно — у модуля будет куча репозиториев для общения с бэком, магазинами и т.п. При этом фичи могут проверять, можно ли что-то показывать при данном лицензионном состоянии, можно ли запускать сервисы, отображать ли кнопки и т.п. Т.е. фича биллинга — она именно фича и от неё могут многие зависеть.
И даже модуль с набором экранов — от него тоже могут зависеть другие модули. Эти экраны-то ведь нужно запускать. И могут быть общие экраны, например, фрагмент камеры. От него могут зависеть несколько других фичей.
В моём понимании, утилитарный модуль — это модуль, которому действительно ничего не нужно. Он содержит только общие функции и у него нет зависимостей, ни на что, даже на контекст приложения.
Чтобы фича А воспользовалась фичёй Б, нужно:
1. Чтобы у фичи А был свой компонент и ComponentHolder.
2. Фича А декларирует в своих зависимостях интерфейс(ы) из фичи Б.
3. При инициализации ComponentHolder'а фичи А, мы прописываем, что она зависит от фичи Б и отдаём ей нужные интерфейсы из фичи Б.
4. Когда компонент фичи А будет создан, автоматически создастся и компонент фичи Б.
5. Внутри фичи А мы можем использовать интерфейсы фичи Б, которые были продекларированы в зависимостях. В случае dagger мы можем эти интерфейсы инжектить как любые другие интерфейсы из компонента фичи А.
Конкретно в случае фрагмента можно выдать в API фабрику, которая будет создавать фрагмент. Это есть в примере: github.com/PavelSidyakin/WeatherForecast/tree/refactor_to_multimodule_structure/feature/weather_details/src/main/java/com/example/weather_details
Но реально модули вполне могут зависеть от других «фичевых» модулей или core-модулей.
Например, фичевые модули, от которых, скорее всего, будет зависеть много других модулей: лицензирование (сюда же покупки, биллинг), аналитика/статистика, управление камерой, пуш-нотификации и многие другие. Они утилитарные? Нет! Хотя бы необходимость контекста уже делает модуль не утилитарным. А некоторым ещё и репозитории свои понадобятся.
Плюс есть core-модули, от которых зависит несколько фич.
В общем, такого не бывает, чтобы все модули зависели только от утилитарных.
Да, тут получается портянка кода инициализации. Но её можно разбить на функции и отдельные файлы. Да и нужный код в этой портянке легко находить – достаточно перейти в имплементацию интерфейса зависимостей модуля. Плюс, как упомянуто в статье, порядок инициализации не важен, так что сильно закапываться в этой портянке не придётся.
Ещё, тут не идёт инициализация модулей, т.е. внутренние компоненты модулей не создаются сразу в Application.onCreate(). Здесь идёт инициализация Component Holder’ов, т.е. инициализация способа инициализации модуля. Потом каждый модуль (точнее, внутренний компонент модуля) инициализируется лениво – когда он кому-то понадобится. И уничтожается, когда он никому не нужен.
Ок, давайте рассмотрим, что будет, если каждый модуль будет сам себя инициализировать, т.е. будет сам себя склеивать с другими модулями.
Пусть у нас есть модуль А, который зависит от интерфейсов модуля Б.
Чтобы модуль А себя проинициализировал, он должен проинициализировать модуль Б. А это значит, он будет знать о деталях имплементации модуля Б, и, что самое печальное, зависеть от модуля, который содержит имплементацию модуля Б.
Если в приложении используется подход с разделением модулей на API/Impl для ускорения сборки, то модуль А будет зависеть и от API модуля Б и от его Impl, что сводит на нет цель разбиения на API/Impl – при изменении Impl модуля Б, пересоберётся и модуль А.
Если же мы делегируем склейку кому-то другому – модулю, который знает обо всех, то получится, что модуль А знает только об API модуля Б. И изменение Impl модуля Б не приведёт к пересборке модуля А. На роль «модуля, который знает обо всех» вполне подходит модуль Application. Можно сделать ещё один модуль, который будет склеивать и тоже знать обо всех других модулях, но не очень ясен смысл делать ещё один всезнающий модуль.
В итоге этот подход с чётким выделением интерфейсов FeatureAPI, FeatureDependencies и внешней склейкой позволяет делать модули более чистыми. У модуля чёткое API и чёткие зависимости. И модулю без разницы как будут проставлены имплементации зависимостей. Модуль просто декларирует, что «мне нужно это», а задача простановки зависимостей (склейка) – это не его проблемы.
По поводу «отключаемости комментированием в build.gradle».
Это уже похоже на архитектуру с плагинами, когда в приложение можно динамически добавлять какой-либо функционал. Это можно реализовать с помощью динамической загрузки jar-ника, через AIDL (тогда дополнительный функционал добавляется установкой ещё одной APK) или другим способом. Но такая архитектура довольно-таки сложна в реализации и создавать её нужно только если это реально нужно. В реальных проектах такое встречается редко.
В реальности, модули создаются не для быстрой отключаемости. О целях выделения в модули написано в статье Андрея, упомянутой в начале текущей статьи. Прочитайте, чтобы понять для чего это вообще нужно.
Хоть внутри MVP c Moxy и напоминает MVVM, но снаружи, т.е. для пользователя библиотеки, это чистый MVP.
Отсюда и все преимущества MVP — более тщательное покрытие unit-тестами поведения UI. С MVVM можно протестировать модель, но не как она используется в имплементации.
Например, в случае MVVM, в модели может быть метод типа fun getData(): Data. Мы можем протестировать unit-тестом что вернул этот метод, но не то, как его использует View.
В случае с MVP в presenter не может быть get-методов. View очень глупая и только presenter говорит View что делать, а значит можно тщательнее протестировать поведение UI unit-тестом presenter'а.
Так что если покрытие тестами — не важно, то можно использовать MVVM. Если важно — то лучше MVP.