Search
Write a publication
Pull to refresh

Новый DI фреймворк для DART и Flutter — sputnik_di

Level of difficultyEasy
Reading time4 min
Views2.2K

Кратко

Понадобился 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, просто мы называем его скоуп).

Концепция sputnik_di
Концепция sputnik_di
ЖЦ 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

Tags:
Hubs:
Total votes 2: ↑1 and ↓10
Comments5

Articles