Pull to refresh

Comments 24

Идеальный модуль должен быть легко отключаемым, например, комментированием его в build.gradle. Если это условие не выполняется, то это уже не модуль, а часть монолита.
Applicatioin.onCreate() не предназначен для того, чтобы в нём инициализировать 100500 модулей и компонентов. Модуль может и сам себя проинициализировать через собственный контент-провайдер.
Согласен про Application.onCreate(), инициализировать там миллион статики — такая себе идея. У нас такими провайдерами зависимостей являются части естественного андроидного дерева — фрагменты(точнее мы используем контроллеры) и активити. А в каждом модуле точка входа может произвести поиск по дереву.
Насчёт отключение непонятно, а как тогда что-то из модуля использовать?

Не может же каждый модуль висеть сам по себе в вакууме.
Модуль предоставляет реализацию каких-то действий или реакцию на какие-то события. Таким образом, если создать абстракции Action, Request и Event, и общение между модулями будет происходить только посредством их, то статическая линковка или DI уже не нужны. Достаточно модулю зарегистрировать, что он умеет обрабатывать эти самые Action-ы, Request-ы и Event-ы.
Также можно использовать динамически подключаемые Validator-ы, которые дополнительно могут проверять возможность обработки Action-ов, Request-ов и Event-ов.
Похожим функционалом, но несколько ограниченно, обладают LocalBroadcastManager, EventBus, RxJava.
Такая концепция модулей очень сильно помогает, когда в проекте бизнес-логика очень часто меняется и необходимо постоянно эти модули включать\выключать\заменять.
Но есть и свои минусы…

Идея понятна, но как это использовать я не очень представляю. Можно конкретный пример?


И сразу объяснение от меня — фича, это, условно говоря, экран(или несколько экранов). Соответственно, это фрагмент или несколько фрагментов, вложенных в один рутовый фрагмент этой фичи. Чтобы воспользоваться фичей, нужно реализовать зависимости, и запровайдить их где-то выше по иерархии(в родительском фрагменте или активити, как именно это сделать, это отдельный разговор), а также в нужный момент перейти на экран (инстанцировать фрагмент и положить его во фрагментменеджер). Соответственно, из модуля как минимум торчит фрагмент, и интерфейс, описывающий зависимости.


Нажали кнопку перехода на новый экран, создался фрагмент, нашёл свои зависимости, из них создал себе компонент, и заинжектил себя. Как-то так. Можно такого уровня пример/объяснение?

Пусть у нас есть фича А, которая хочет воспользоваться фичёй Б.

Чтобы фича А воспользовалась фичёй Б, нужно:

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
Спасибо за фидбэк!
По поводу «отключаемости комментированием в build.gradle».
Это уже похоже на архитектуру с плагинами, когда в приложение можно динамически добавлять какой-либо функционал. Это можно реализовать с помощью динамической загрузки jar-ника, через AIDL (тогда дополнительный функционал добавляется установкой ещё одной APK) или другим способом. Но такая архитектура довольно-таки сложна в реализации и создавать её нужно только если это реально нужно. В реальных проектах такое встречается редко.
В реальности, модули создаются не для быстрой отключаемости. О целях выделения в модули написано в статье Андрея, упомянутой в начале текущей статьи. Прочитайте, чтобы понять для чего это вообще нужно.
По поводу инициализации «100500 статиков в Application.onCreate()».

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

Ещё, тут не идёт инициализация модулей, т.е. внутренние компоненты модулей не создаются сразу в Application.onCreate(). Здесь идёт инициализация Component Holder’ов, т.е. инициализация способа инициализации модуля. Потом каждый модуль (точнее, внутренний компонент модуля) инициализируется лениво – когда он кому-то понадобится. И уничтожается, когда он никому не нужен.

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

Пусть у нас есть модуль А, который зависит от интерфейсов модуля Б.

Чтобы модуль А себя проинициализировал, он должен проинициализировать модуль Б. А это значит, он будет знать о деталях имплементации модуля Б, и, что самое печальное, зависеть от модуля, который содержит имплементацию модуля Б.

Если в приложении используется подход с разделением модулей на API/Impl для ускорения сборки, то модуль А будет зависеть и от API модуля Б и от его Impl, что сводит на нет цель разбиения на API/Impl – при изменении Impl модуля Б, пересоберётся и модуль А.

Если же мы делегируем склейку кому-то другому – модулю, который знает обо всех, то получится, что модуль А знает только об API модуля Б. И изменение Impl модуля Б не приведёт к пересборке модуля А. На роль «модуля, который знает обо всех» вполне подходит модуль Application. Можно сделать ещё один модуль, который будет склеивать и тоже знать обо всех других модулях, но не очень ясен смысл делать ещё один всезнающий модуль.

В итоге этот подход с чётким выделением интерфейсов FeatureAPI, FeatureDependencies и внешней склейкой позволяет делать модули более чистыми. У модуля чёткое API и чёткие зависимости. И модулю без разницы как будут проставлены имплементации зависимостей. Модуль просто декларирует, что «мне нужно это», а задача простановки зависимостей (склейка) – это не его проблемы.

По-хорошему, А от Б может зависеть только если Б — утилитарный, а не фичевый модуль. А это значит, что его не нужно инициализировать.

Это было бы идеально, если бы все модули могли зависеть только от утилитарных модулей.
Но реально модули вполне могут зависеть от других «фичевых» модулей или core-модулей.
Например, фичевые модули, от которых, скорее всего, будет зависеть много других модулей: лицензирование (сюда же покупки, биллинг), аналитика/статистика, управление камерой, пуш-нотификации и многие другие. Они утилитарные? Нет! Хотя бы необходимость контекста уже делает модуль не утилитарным. А некоторым ещё и репозитории свои понадобятся.
Плюс есть core-модули, от которых зависит несколько фич.

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

У нас все модули, которые вы перечислили, это утилитарные модули. Биллинга у нас нет, но если там набор отдельных экранов, то это фиче-модуль, и от него напрямую другие фиче-модули зависеть не будут.
Да, иногда возникает желание позависеть напрямую, но ничем хорошим это, по моему опыту, не кончалось.

По-хорошему — нет, они не утилитарные.

Взять хоть нотификации. Для показа нужен контекст приложения. Что уже делает этот модуль не утилитарным. Ему нужно в зависимости предоставлять контекст, а лучше вообще интерфейс репозитория, чтобы не зависеть от андроидских классов (фича тогда будет кроссплатформенной).

Биллинг (повезло, что у вас его нет :)). Тут понятно — у модуля будет куча репозиториев для общения с бэком, магазинами и т.п. При этом фичи могут проверять, можно ли что-то показывать при данном лицензионном состоянии, можно ли запускать сервисы, отображать ли кнопки и т.п. Т.е. фича биллинга — она именно фича и от неё могут многие зависеть.

И даже модуль с набором экранов — от него тоже могут зависеть другие модули. Эти экраны-то ведь нужно запускать. И могут быть общие экраны, например, фрагмент камеры. От него могут зависеть несколько других фичей.

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

Спасибо за статью! Интересно, есть на чем подумать, мб получиться решить проблемы, сделав гибридный вариант. Не могу не спросить что означает "прикопает"? Di -> dig -> прикопает? Это какой-то слэнг? Напомнило мне старый анекдот. Мужчина просить девушку передать за проезд в автобусе:
- Будьте добры, рыбанька, передайте пожалуйста
Девушка думает: "Рыбанька значит щука, щука значит зубастая, зубастая значит собака":
- А-о! Товарищи, он меня с*чкой обозвал!

Это действительно слэнг :)) Означает что-то типа "заберёт себе". В данном случае имеется в виду "будет держать у себя" ссылку. Английский аналог "hold".

Доброе утро) По ходу вхождения в многомодульность я несколько раз перечитал вашу статью (это крутая!), но у меня все-равно есть один неразрешенный вопрос: допустим, клиент находиться на каком-то экране, в этот момент запускается сборщик мусора - получиться, что ссылка на компонент будет удалена из holder-а. Если для клиента этот модуль был модуль с состоянияем, которое должно было использовать в следующем модуле, возникнет проблема - ведь во втором модуле мы получим уже другой объект компонента, т.к. weakReference занулиться. Получается, для модулей, которые хранят состояния и подключается между несколькими фичами нельзя использовать мягкую ссылу и нужно вычищать их ручками? Как быть?

P.S. у меня этот вопрос архи-важный, т.к. приложение работает нон-стоп с момента включения планшета и его выключения

Доброе утро :)

Насколько я понимаю, у Вас такая ситуация: есть экран А, он отображается и где-то меняет состояние. Потом погибает и отображается экран Б, который должен поюзать это состояние.

Если ссылка на компонент, где живёт это состояние, закопана только в компонентах экранов, то да, состояние может потеряться (зависит от того, как быстро GC его грохнет).

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

Я бы рекомендовал ещё подумать насчёт базы на data уровне. Тогда всё просто: модифицируем базу и изменения доступны для всех фичей. Можно даже не базу, а просто сеттинги. Или даже просто в памяти хранить. Самое важное - на data уровне и закрыть имплементацию репозиторием. Я делал такой пример: https://github.com/PavelSidyakin/ProductList Там Like шарится между экранами.

и за это спасибо, наверное меня увлекли задачи, раз я пропустил ответ

@foxspa Спасибо за статью - буквально по мне проводим рефакторинг! Скажите, как вы подключаете вьюмодели в фича модулях? Фабрика уходит в коммон и подключается в каждый модуль в отдельности? Откуда тогда локально брать мапу viewModel-провайдеров? А если подключиние строго в app-module - прокидываете объект фабрики в модуль? Очень интересно как у вас организована работа с фабрикой вьмоделей, ломаю над этим голову второй день. В ваших классных примерах viewModel-и почему-то не представлены.

Здравствуйте!

ViewModel должна жить в модуле вместе со своим экраном и быть internal. Нет смысла куда-то выносить фабрики VM в другой модуль.

Создание VM будет аналогично созданию презентера в примере. Или в этом примере - созданию контроллера. Вы, возможно, захотите использовать в фрагменте "by viewModels()" с кастомной фабрикой для создания VM.

В общем, не усложняйте. Попробуйте сделать VM internal и создавать кастомной фабрикой. Пример не подскажу, к сожалению.

У нас в проекте фабрика общая - реализована через map:

class CarolViewModelFactory @Inject constructor(    private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {

В мапу добавляется по ключу:

@Target(    AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)@Retention(AnnotationRetention.RUNTIME)@MapKeyannotation class ViewModelKey(val value: KClass<out ViewModel>)

отсюда и возникает делема: чтобы добавить вьюмодель фичи в мапу - должна существовать мапа, а она вроде как живет только в app. Можно попробовать разрулить интерфесами, но уж больно их много получится. Если у вас есть время, пожалуйста, сделайте для меня примерчик - торжественно клянусь помочь 10 другим ребятам на SO)))

да, спасибо за примеры - посмотрю их сейчас

примеры посмотрел, но это совсем не то. Здесь локальные закрытые фабрики для локальных закрытых объектов - никакой связи с app или чем-то подобным

Оно так и должно быть. Сущности, управляющие View (presenter, controller, VM, и т.п.) не должны выходить за пределы своего модуля и app не должен о них знать. Чем меньше модули знают друг о друге - тем лучше. В целом, если что-то может быть internal, то оно и должно таким быть.

Если очень хотите сделать через фабрику с мапой из app, то тут, похоже, без каких-либо интерфейсов в общем модуле не сделать. Но решение выглядит странным imho.

Честно говоря, странно что вы меня так плохо понимаете. Вопроса по инкапсулции не стоит! Я как раз пытаюсь все закрыть и запрятать - уже раз 10 это в разных формах Вам повторил. Естественно меня одна фабрика, ведь я разношу app-монолит по модулям. Жаль, очевидно вы работаете с MVP, а значит вопрос с общей фабрикой у вас не стоит - превращать одну фабрику в кучу болер плейт кода без крайней нужнды я не спешу, уж извините. В любом случае спасибо что хоть что-то написали

Sign up to leave a comment.

Articles