баннер
баннер

при работе с Dart и Flutter становится очевидным: многие DI-библиотеки либо слишком тяжёлые, либо слишком простые. одни предлагают автоматическую магию, скрытые зависимости, runtime-рефлексию - что усложняет тестирование и снижает производительность. другие дают лишь базовый функционал, который не покрывает типичные сценарии: scoped-контейнеры, несколько реализаций одного интерфейса, декораторы, модули

в такой ситуации мне понадобился DI-контейнер, отвечающий следующим требованиям:

  • минимальный API, без магии

  • высокая производительность

  • явно управляемый жизненный цикл зависимостей

  • поддержка real-world сценариев (scoped-контейнеры, декораторы, ключевые реализации, модули)

на основе этих требований я разработал DRTDI

задачи, которые должен был решать DRTDI

при проектировании DRTDI я сразу сформулировал основные задачи:

  1. прозрачность - контейнер не должен делать ничего, на что пользователь не дал явного указания. Никакой мета-магии, автоматического сканирования или кодогенерации

  2. производительность - разрешение зависимостей должно быть быстрым, без рефлексии, proxy-классов, dynamic code

  3. гибкость - контейнер должен поддерживать:

    • несколько жизненных циклов (singleton, transient, scoped)

    • иерархию контейнеров (parent/child)

    • регистрацию нескольких реализаций одного интерфейса с ключами (keyed registrations)

    • возможность применения декораторов к зарегистрированным сервисам

    • группировку регистраций в модули для структурирования архитектуры

  4. универсальность - работа как в обычных Dart-проектах, так и в приложениях на Flutter

основные концепции DRTDI

жизненные циклы (Lifetimes)

  • singleton - единый объект на весь жизненный цикл контейнера

  • transient - новый экземпляр при каждом разрешении

  • scoped - жизненный цикл ограничен scope-контейнером; при создании дочернего контейнера - создаётся новый экземпляр

благодаря этому можно гибко управлять временем жизни объектов и их изоляцией

иерархия контейнеров

контейнеры могут быть организованы в дерево: есть корневой контейнер, дочерние, и т. д

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

keyed registrations — несколько реализаций одного интерфейса

поддержка ключей (например, строковых или enum) при регистрации позволяет иметь несколько реализаций одного интерфейса, например:

container.register<Storage>((c) => FileStorage(), key: 'file');
container.register<Storage>((c) => CloudStorage(), key: 'cloud');

при разрешении можно явно указать, какая реализация нужна:

final fileStorage = container.resolve<Storage>(key: 'file');
final cloudStorage = container.resolve<Storage>(key: 'cloud');

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

декораторы (Decorators / Middlewares)

после создания объекта контейнер может "обернуть" его в дополнительный слой - например, для логирования, кэширования, тайминга, проверки прав доступа и т. д.:

container.decorate<ApiClient>((original) {  
  return LoggingApiClient(original);
});

благодаря этому можно легко расширять поведение сервисов без изменения их исходной реализации

модули (модульная регистрация зависимостей)

DRTDI позволяет группировать регистрации по логическим модулям, что делает конфигурацию DI чистой и поддерживаемой. например, модуль для сетевого слоя, модуль для хранения данных, модуль для бизнес-логики и т. п

как реализовано внутри - уход от магии, ставка на простоту и скорость

при реализации DRTDI я сознательно отказался от:

  • runtime-рефлексии

  • автоматического сканирования классов

  • code-generation

  • dynamic proxy / runtime-сборки

всё, что делает контейнер - это хранит маппинг типов (и ключей) → фабрик, и при запросе выполняет соответствующую фабрику, возвращая объект. при необходимости - применяет декораторы или возвращает ранее созданный singleton

такой подход даёт:

  • детерминированность: поведение контейнера полностью предсказуемо

  • минимальные накладные расходы: нет overhead от рефлексии или кодогенерации

  • простоту понимания / отладки: можно легко отследить, где и что создаётся

  • гибкость: расширения (декораторы, ключи, scoped) легко реализуются без сложной инфраструктуры

использование: примеры кода

регистрация и разрешение простых сервисов

final container = Container();
// регистрация singleton
container.registerSingleton<Config>(() => Config());
// регистрация transient
container.registerTransient<UserService>(() => UserService(  
  container.resolve<Config>(),
));
// разрешение
var config = container.resolve<Config>();
var userService = container.resolve<UserService>();

scoped-контейнеры

final root = Container();
root.registerSingleton<GlobalService>(() => GlobalService());
final screenScope = root.createChild();
screenScope.registerTransient<ScreenService>(() => ScreenService(root.resolve<GlobalService>())
);
var screenService = screenScope.resolve<ScreenService>();

keyed реализации

root.register<Storage>(() => FileStorage(), key: 'file');
root.register<Storage>(() => CloudStorage(), key: 'cloud');
var fileStorage = root.resolve<Storage>(key: 'file');
var cloudStorage = root.resolve<Storage>(key: 'cloud');

декораторы

root.register<ApiClient>(() => ApiClientImpl());
root.decorate<ApiClient>((client) {  
  return LoggingApiClient(client);
});
var api = root.resolve<ApiClient>();
// api — это LoggingApiClient, оборачивающий ApiClientImpl

почему DRTDI может быть полезен на практике

  • минимальный overhead - контейнер не добавляет лишних абстракций, runtime-прокси или магии

  • предсказуемость поведения - всё делается явно, видно, какие зависимости зарегистрированы, как они создаются и когда

  • гибкость для разных задач - singleton, scoped, transient; ключевые реализации; декораторы; модули

  • лёгкость сопровождения и понимания архитектуры - конфигурация DI остаётся прозрачной, лёгкой для чтения и рефакторинга

  • универсальность - библиотека пригодна как для небольших утилит (CLI), так и для крупных Flutter-приложений

почему проект открыт - и как можно использовать DRTDI

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

исходники доступны в публичном репозитории:
https://github.com/C0dwiz/drtdi

цель - дать разработчикам инструмент, который:

  • остаётся лёгким

  • даёт контроль

  • не навязывает сложную инфраструктуру

  • остаётся быстрым и предсказуемым

если вам нужен DI-контейнер, который не усложняет проект, а служит надёжной и простой основой для зависимостей - возможно, DRTDI вам подойдёт

заключение

DRTDI - это попытка предложить DI без компромиссов: минимальный API, высокая производительность, гибкая архитектура

если вы цените:

  • прозрачность зависимостей и конфигурации

  • контроль над жизненным циклом объектов

  • простоту и лёгкость сопровождения

  • производительность и отсутствие лишних runtime-зависимостей

то DRTDI - достойный вариант

я открыт к вопросам, предложениям, PR и обсуждениям. возможно, вместе удастся сделать DI в экосистеме Dart чуть удобнее и надёжнее

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


  1. serg_dominator
    16.12.2025 05:35

    И чем он отличается от известного и уже существующего get_it?

    Из текста складывается впечатление, что ничем.


  1. alexlapin
    16.12.2025 05:35

    Автор, если не сложно, напиши в двух словах зачем эта штука мне нужна и почему существующие решения мне не подойдут.

    Спасибо.