Кратко
Понадобился DI без бойлерплейта, который не позволяет достать что угодно откуда угодно. Написал два пакета: sputnik_di для dart и flutter_sputnik_di для flutter. Кайфую.
Для Dart:
dart pub add sputnik_di
Для Flutter:
dart pub add flutter_sputnik_di
По сути зависимости представляют собой оринетированный граф. DepsNode - узел зависимостей, в которых регистрируются все необходимые классы и описывает порядок их сборки.
Мотивация
Сейчас у нас на рынке есть три основных вариантов: get_it, riverpod и yx_scope. Они покрывают практически все потребности и у каждого есть свои плюсы и минусы.
get_it довольно громоздкий и тяжело работает со scope'ами, кроме того он позволяет достать любую зависимость из любого места.
riverpod я активно использую и считаю, что это один из самых удачных вариантов, если использовать его правильно. Однако позволяет подключать любую зависимость в любой контейнер, что пугает. Кроме того, автор фреймворка впихнул туда довольно много лишних концепций, и направление развития в общем мне не нравится.
yx_scope действительно хорош, я иногда пишу на нем модули приложения и из моего опыта - приходится писать много кода. Создай scope_holder, создай контейнер. Сложно прокинуть зависимости извне. Много путаницы с базовым классом ScopeContainer. В общем порог входа туда довольно высокий, много бойлерплейта.
Захотелось создавать легковесный фреймворк с максимально простой концепцией, при этом сохранить баланс между бойлерплейтом и качеством кода. Моя мысль при создании этого пакета была такая: ошибся - сам дурак. Я рассчитываю на то, что разработчик достаточно умен, чтобы уметь структурировать зависимости по контейнерам и правильно организовывать последовательность их вызовов. Как говорится, нормально делай - нормально будет.
Концепция
Я не люблю в голове держать одновременно много вещей. Я вообще по своей сути ленивый. Поэтому хотел, чтобы запоминать надо было как можно меньше.
Есть узел зависимостей DepsNode. Все. Дальше крутите как хотите.
Ну а если погрузиться чуть глубже, DepsNode - это узел из ориентированного графа ваших зависимостей. Узел может быть геморроидальным представлять какую-либо фичу или быть так называемым скоупом. Все зависит от вас. В конце концов, все узлы формируют граф, который может быть разделен узлами-скоупами (которые по сути тоже являются DepsNode, просто мы называем его скоуп).


DepsNode представляет собой класс, в полях которого описываются зависимости. Он имеет не сложный ЖЦ.
Простой пример
Напишем простой пример с использованием sputnik_di для реализации каунтера.
class CounterDepsNode extends DepsNode {
late final counterManager = bind(
() => CounterManager(
counterStateHolder(),
),
);
late final counterStateHolder = bind(
() => CounterStateHolder(),
);
// Очередь этих зависимостей проинициализируется когда будет вызвано
// depsNode.init();
@override
List<Set<LifecycleDependency>> get initializeQueue = [
{
counterStateHolder,
},
];
}
class CounterManager {
final CounterStateHolder _counterStateHolder;
const CounterManager(this._counterStateHolder);
void increaseBy(int count) {
for (int i = 0; i < count; i++) {
_counterStateHolder.increment();
}
}
}
// StateHolder - встренный в фреймворк класс, хранящий в себе состояние
class CounterStateHolder extends StateHolder<int> {
CounterStateHolder() : super(0);
void increment() {
state = state + 1;
}
}
Future<void> main() async {
final counterDepsNode = CounterDepsNode();
// инициализируем узел
await counterDepsNode.init();
counterDepsNode.counterManager().increaseBy(5);
print(counterDepsNode.counterStateHolder().state); // выведет 5
// высвобождаем ресурсы (подписки и др)
await counterDepsNode.dispose();
// вы можете очистить все созданные внутри экземпляры классов
// которые были обернуты в bind или bindSingletonFactory
counterDepsNode.clear();
}
Понятно, что таким образом можно вкладывать узлы друг в друга и формировать граф:
class AuthScopeDepsNode {
final AppScopeDepsNode _appScopeDepsNode;
AuthScopeDepsNode(this._appScopeDepsNode);
late final counterDepsNode = bind(() => CounterDepsNode());
@override
List<Set<LifecycleDependency>> get initializeQueue = [
{
counterDepsNode,
},
];
}
class AppScopeDepsNode {
late final authScopeDepsNode = bind(() => AuthScopeDepsNode(this));
late final authManager = bind(
() => AuthManager(
authScopeDepsNode(),
)
);
}
Использование с Flutter
Вернемся к примеру с CounterDepsNode
и попробуем на его основе показать связку Flutter с sputnik_di.
Прокинуть DepsNode вниз по дереву DepsNodeBinder:
final counterDepsNode = CounterDepsNode();
DepsNodeBinder(
depsNode: counterDepsNode,
child: Builder(
builder: (context) {
final variant1 = context.depsNode<CounterDepsNode>();
final variant2 = DepsNodeBinder.of<CounterDepsNode>(context);
...
}
),
)
Билдер на основе DepsNode - DepsNodeBuilder:
DepsNodeBuilder(
depsNode: counterDepsNode,
bindOnInitialized: false,
idle: (context, depsNode) {...},
initializing: (context, depsNode) {...},
// required
initialized: (context, depsNode) {...},
disposing: (context, depsNode) {...},
disposed: (context, depsNode) {...},
// required
orElse: (context, depsNode) {...},
)
Билдер на основе StateHolder, StateHolderBuilder:
final counterStateHolder = counterDepsNode.counterStateHolder();
StateHolderBuilder(
holder: counterStateHolder,
builder: (context, state) {...}
)
Слушатель на основе StateHolder, StateHolderListener:
StateHolderListener(
holder: counterStateHolder,
listener: (state) {
...
},
child: ...,
)
Все кажется довольно просто, мы видели это много раз.
Фабрики
В фреймворке есть возможность создавать зависимости на основе параметров и выглядит это так:
class AuthScopeDepsNode {
late final orderScope = bindSingletonFactory(
(String orderUuid) => OrderScopeDepsNode(orderUuid),
);
}
class OrderScopeDepsNode {
final String orderUuid;
OrderScopeDepsNode(this.orderUuid);
...
}
Таким образом можно хранить множество заказов друг за другом под одной зависимостью и получать контексты для разных заказов по необходимости.
Что с этими знаниями делать?
Все вам рассказал, круто и классно. Делаем dart pub add flutter_sputnik_di|sputnik_di
и используем с удовольствием. Жду вас в своем репозиториями с фидбеком и предложениями: github.
Заключение
Получился довольно лаконичный пакет, который ничего не навязывает. При этом этот пакет подойдет для проектов разных уровней сложности. Порог входа в такой пакет минимальный. Пишите ваши предложения, оставляйте отзывы, делитесь своими примерами использования. Всем хорошего настроения, всем добра :-)
Мой тг канал: @gubin_dev
dkfbm
Я, конечно, дико извиняюсь, но первое, что вижу в коде – жёстко прописанные зависимости
CounterDepsNode
отCounterManager
иCounterStateHolder
. Как это мокать, например?webmadness Автор
Тут очень просто все мокать. Достаточно создать интерфейс для необходимой зависимости
Более того, ты можешь сделать родителем для AuthScope какой-либо интерфейс и портировать основную логику приложения от родителя к родителю
Спасибо за вопрос!
dkfbm
В дарт каждый класс и так интерфейс. От добавления буковки
I
абсолютно ничего не меняется. Я не вижу в примере использования якобы DI фреймворка собственно инжекции зависимостей, в коде они жёстко зашиты в сам класс этого разнесчастного счётчика. Передать ему мок зависимости (скажем, стэйта) при тестировании я не могу. Дальше просто не разбирался, этого достаточно. Тем более, всё остальное очень уж напоминает FizzBuzz Enterprise Edition.webmadness Автор
Благодарю за твое предложение! Я внес изменения в пакет. Добавил возможность перезаписывать зависимость с помощью метода overrideWith, выглядит это так:
Кроме того, я добавил этот раздел в README пакета, будет проще разобраться, ссылочка: https://pub.dev/packages/flutter_sputnik_di#mocking-dependencies-with-overridewith
Если у тебя будут еще какие-нибудь вопросы или пожелания, можешь писать мне. Мои контакты есть в профиле. Спасибо.
Добра и любви)
dkfbm
Собственно, одно пожелание, но сразу по двум совсем разным пунктам: прежде чем начинать чем-то заниматься, ознакомиться с историей вопроса:
Перед тем, как разрабатывать (и тем более, рекламировать) собственный DI фреймворк, понять, что же такое, собственно, DI. Ибо предложенное решение именно этого-то и не делает. А с последними изменениями делает криво – по прозрачности кода и количеству бойлерплэйта никакого сравнения с популярными пакетами.
Выходя на популярный ресурс, ознакомиться с принятым на нём стилем общения. Тут принято обращаться к незнакомым на Вы. Мне лично пофиг, но выглядит тут необычно, и насколько я видел, когда встречается, многих раздражает.