Кратко

Понадобился 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

Комментарии (5)


  1. dkfbm
    07.02.2025 20:03

    Я, конечно, дико извиняюсь, но первое, что вижу в коде – жёстко прописанные зависимости CounterDepsNode от CounterManager и CounterStateHolder. Как это мокать, например?


    1. webmadness Автор
      07.02.2025 20:03

      Тут очень просто все мокать. Достаточно создать интерфейс для необходимой зависимости

      class CounterDepsNode extends DepsNode {
        late final counterManager = bind<ICounterManager>(
          () => CounterManager(
            counterStateHolder(),
          ),
        );
        ...
      }
      
      abstract interface class ICounterManager {
        void increaseBy(int count);
      }
      
      class CounterManager implements ICounterManager {
        final CounterStateHolder _counterStateHolder;
      
        const CounterManager(this._counterStateHolder);
      
        @override
        void increaseBy(int count) {
          for (int i = 0; i < count; i++) {
            _counterStateHolder.increment();
          }
        }
      }

      Более того, ты можешь сделать родителем для AuthScope какой-либо интерфейс и портировать основную логику приложения от родителя к родителю

      class AuthScopeDepsNode {
        final IBaseAppScope _appScope;
        
        AuthScopeDepsNode(this._appScope);
        
        late final counterDepsNode = bind(() => CounterDepsNode());
      
        @override
        List<Set<LifecycleDependency>> get initializeQueue = [
          {
            counterDepsNode,
          },
        ];
      }
      
      class IBaseAppScope {
        AuthManager get authManager;
      }
      
      class AppScopeDepsNode implements IBaseAppScope {
        late final authScopeDepsNode = bind(() => AuthScopeDepsNode(this));
      
        @override
        late final authManager = bind(
          () => AuthManager(
            authScopeDepsNode(),
          )
        );
      }

      Спасибо за вопрос!


      1. dkfbm
        07.02.2025 20:03

        Достаточно создать интерфейс для необходимой зависимости

        В дарт каждый класс и так интерфейс. От добавления буковки I абсолютно ничего не меняется. Я не вижу в примере использования якобы DI фреймворка собственно инжекции зависимостей, в коде они жёстко зашиты в сам класс этого разнесчастного счётчика. Передать ему мок зависимости (скажем, стэйта) при тестировании я не могу. Дальше просто не разбирался, этого достаточно. Тем более, всё остальное очень уж напоминает FizzBuzz Enterprise Edition.


        1. webmadness Автор
          07.02.2025 20:03

          Благодарю за твое предложение! Я внес изменения в пакет. Добавил возможность перезаписывать зависимость с помощью метода overrideWith, выглядит это так:

          void main() {
            final depsNode = FeatureDepsNode();
          
            // Override dependency with a mock
            depsNode.featureManager.overrideWith(() => MockFeatureManager());
          
            final featureManager = depsNode.featureManager();
          
            // Use featureManager in tests
          }

          Кроме того, я добавил этот раздел в README пакета, будет проще разобраться, ссылочка: https://pub.dev/packages/flutter_sputnik_di#mocking-dependencies-with-overridewith

          Если у тебя будут еще какие-нибудь вопросы или пожелания, можешь писать мне. Мои контакты есть в профиле. Спасибо.

          Добра и любви)


          1. dkfbm
            07.02.2025 20:03

            Если у тебя будут еще какие-нибудь вопросы или пожелания

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

            1. Перед тем, как разрабатывать (и тем более, рекламировать) собственный DI фреймворк, понять, что же такое, собственно, DI. Ибо предложенное решение именно этого-то и не делает. А с последними изменениями делает криво – по прозрачности кода и количеству бойлерплэйта никакого сравнения с популярными пакетами.

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