agentleFS
ariaraminariaramin/flutter-skills/skills/flutter-get-it-injectable-dependency-injection/SKILL.md

flutter-get-it-injectable-dependency-injection

Skill8 starsChanged 48 days agoMITView on GitHub

Design, implement, review, debug, test, refactor, and optimize dependency injection in production Flutter/Dart repositories using get_it, injectable, or both. Use this skill whenever work involves adding or wiring services/repositories/API clients/databases/SDKs/Bloc/Cubit dependencies, choosing factory/singleton/lazy/async/scoped lifetimes, configuring injectable generation/modules/environments/named bindings, managing startup readiness, login/logout session scopes, disposal, DI tests/overrides, circular dependencies, or hidden GetIt.I lookups—even if the request is framed only as “register this service”, “fix DI”, “wire this dependency”, or “make startup cleaner”.

The file

Show its rules and text

We split this skill into the rules it gives an agent

60 rules. About 0 are common, 39 are distinctive, and 21 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈39

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 21

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Very specific:

    Treat get_it as the runtime container, injectable as optional registration generation, and constructor injection as the application-facing …

    line 13
  2. Distinctive:

    Do not introduce DI machinery when direct constructors are already clearer.

    line 15
  3. Distinctive:

    Prefer constructor injection in application/business code.

    line 20
  4. Very specific:

    Keep direct getIt<T>() access at composition/framework boundaries.

    line 21
  5. Distinctive:

    Do not make everything a singleton or every Bloc/Cubit a singleton.

    line 24
  6. Very specific:

    Use scopes for real session/account/workflow lifetime boundaries.

    line 25
  7. Distinctive:

    Keep environment selection centralized.

    line 28
  8. Distinctive:

    Use named registrations only when same-type instances genuinely coexist.

    line 29
  9. Distinctive:

    Do not hide circular dependencies with locator lookup.

    line 30
  10. Very specific:

    Never edit generated *.config.dart manually.

    line 31
  11. Distinctive:

    every registration, scope, readiness, reset, and direct locator lookup.

    line 54
  12. Distinctive:

    Do not refactor until the graph is understandable.

    line 71
  13. Distinctive:

    Use direct construction for cheap pure/local helpers and transient values that do not belong to the application object graph.

    line 87
  14. Very specific:

    Avoid business classes that resolve GetIt.I<T>() internally.

    line 101
  15. Distinctive:

    Use method parameters for call-specific data.

    line 103
  16. Distinctive:

    Do not choose singleton merely to avoid wiring constructors.

    line 118
  17. Very specific:

    Use constructors/manual get_it when the graph remains easy to read.

    line 122
  18. Very specific:

    Use injectable when generated ordering, modules, environments, scopes, or many transitive registrations meaningfully reduce boilerplate.

    line 124
  19. Distinctive:

    Use factory params or ordinary constructor/method arguments for route IDs, workflow values, or call data.

    line 130
  20. Distinctive:

    Do not turn changing IDs/user input into global singleton registrations.

    line 132
  21. Distinctive:

    Prefer Future-based async registration to manual readiness signals when possible.

    line 147
  22. Very specific:

    Never register from widget build, repository methods, or opportunistic business flows.

    line 169
  23. Distinctive:

    Use for route/feature-local mutable objects and most Bloc/Cubit registrations.

    line 175
  24. Very specific:

    Use for shared services/repositories/infrastructure that need one instance but not eager startup creation.

    line 179
  25. Very specific:

    Use registerSingletonAsync for shared async creation.

    line 183
  26. Very specific:

    Use dependsOn only for real dependency ordering.

    line 185
  27. Very specific:

    Use registerSingletonWithDependencies when construction is synchronous but prerequisites are async.

    line 187
  28. Very specific:

    Use allReady() only for the required async graph and consider a timeout.

    line 193
  29. Very specific:

    Do not use manual signalReady where a Future can represent readiness.

    line 195
  30. Distinctive:

    Use scopes for coherent lifetime groups:

    line 199
  31. Distinctive:

    Use names for same-type objects that genuinely coexist, such as public and authenticated Dio.

    line 212
  32. Distinctive:

    Use environment bindings for mutually exclusive implementations.

    line 214
  33. Distinctive:

    Treat annotations as registration metadata, not architecture.

    line 220
  34. Very specific:

    Do not use allReady() as a universal startup hammer.

    line 246
  35. Distinctive:

    Keep app-global infrastructure separate from user-specific state.

    line 250
  36. Distinctive:

    Use app-lifetime Bloc/Cubit only when state truly has application lifetime.

    line 279
  37. Very specific:

    Do not inject GetIt into Bloc/Cubit business logic.

    line 281
  38. Distinctive:

    Do not close app-level infrastructure from a route.

    line 291
  39. Very specific:

    Treat A → B → A as a design smell.

    line 295
  40. Very specific:

    Do not hide the cycle using lazy GetIt.I getters.

    line 297
  41. Very specific:

    Use GetIt.asNewInstance() for an isolated graph.

    line 307
  42. Distinctive:

    Use scope shadowing for bounded overrides of an existing graph.

    line 309
  43. Distinctive:

    Use a generated test environment when testing environment/configuration behavior, not ordinary business logic.

    line 315
  44. Very specific:

    Avoid allowReassignment as the default testing strategy because it can mask duplicate-registration mistakes.

    line 317
  45. Distinctive:

    Use the repository's established generation command, commonly:

    line 321
  46. Distinctive:

    After DI graph changes during Flutter development, use hot restart when the running container must be rebuilt.

    line 331
  47. Distinctive:

    Do not micro-optimize ordinary type/name lookup before these costs.

    line 344
  48. Distinctive:

    Do not use weak cached factories for resources requiring deterministic disposal.

    line 346
  49. Distinctive:

    Check composition init, generated freshness, binding type, environment, scope, name, and async initialization.

    line 352
  50. Distinctive:

    Check bootstrap called twice, widget/runtime registration, manual + generated duplication, overlapping environments, duplicate names, or …

    line 356
  51. Very specific:

    Check get<T>() vs getAsync<T>(), missing awaited generated init/pre-resolution, dependsOn, manual readiness, and async cycles.

    line 360
  52. Distinctive:

    Update manual/generated registration.

    line 374
  53. Distinctive:

    Add a direct constructor unit test.

    line 375
  54. Distinctive:

    Remove legacy locator access after consumers are migrated.

    line 377
  55. Distinctive:

    Keep DI refactoring separate from unrelated domain behavior changes where possible.

    line 379
  56. Distinctive:

    all required dependencies resolve.

    line 402
  57. Distinctive:

    no accidental duplicate or hidden cycle.

    line 403
  58. Distinctive:

    every disposable resource has a known owner.

    line 415
  59. Distinctive:

    Before accepting any registration, answer:

    line 445
  60. Distinctive:

    Only then choose plain construction, factory, singleton, lazy singleton, async registration, scope, module, environment binding, or name.

    line 458

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Flutter Dependency Injection with get_it & injectable

Mission

Build a dependency graph whose ownership, lifetime, readiness, environment, and cleanup are explicit.

Treat get_it as the runtime container, injectable as optional registration generation, and constructor injection as the application-facing dependency contract.

Do not introduce DI machinery when direct constructors are already clearer.

Invariants

  1. Define ownership before registration.
  2. Prefer constructor injection in application/business code.
  3. Keep direct getIt<T>() access at composition/framework boundaries.
  4. Centralize registration in bootstrap or explicit scope initialization.
  5. Choose lifetime from semantics, not convenience.
  6. Do not make everything a singleton or every Bloc/Cubit a singleton.
  7. Use scopes for real session/account/workflow lifetime boundaries.
  8. Distinguish construction, async initialization, and readiness.
  9. Define deterministic disposal for owned resources.
  10. Keep environment selection centralized.
  11. Use named registrations only when same-type instances genuinely coexist.
  12. Do not hide circular dependencies with locator lookup.
  13. Never edit generated *.config.dart manually.
  14. Let narrow unit tests bypass the container.
  15. Optimize eager work and retained graphs before lookup micro-performance.

Reference routing

NeedRead
DI choice, get_it vs injectable, lifetimes, constructor injection, modules, names, environmentsreferences/01-composition-lifetimes.md
Async registration/readiness, scopes, session lifetime, disposalreferences/02-async-scopes-disposal.md
Dio/Bloc/repository integration, startup, tests/overrides, codegen, registration diagnosisreferences/03-integrations-startup-testing.md
Circular dependencies, performance, memory, hot reload, organization, anti-patternsreferences/04-architecture-performance-antipatterns.md
Implementation/review/refactor workflows and production checklistsreferences/05-workflows-checklists.md

Load the smallest combination that resolves the task.

Repository inspection

Before editing, inspect:

  • pubspec.yaml, pubspec.lock, SDK constraints, and locked DI package versions;
  • main, bootstrap, configureDependencies, @InjectableInit;
  • generated *.config.dart to understand current registrations, without editing it;
  • every registration, scope, readiness, reset, and direct locator lookup;
  • lifecycle owners for Dio, DBs, sockets, subscriptions, caches, SDKs, Bloc/Cubit;
  • environment bindings, names, modules, async/pre-resolve/dispose annotations;
  • tests that override, shadow, or reset registrations;
  • the repository's generation commands.

For important registrations capture:

Type / implementation
Owner / lifetime / scope
Name / environment
Async readiness
Disposal
Consumers

Do not refactor until the graph is understandable.

Dependency decision workflow

1. Identify responsibility

State what the object does. If unclear, fix the responsibility before DI design.

2. Identify the smallest real owner

Choose application, session/account/tenant, feature/workflow, route/widget subtree, or one call.

Ownership determines lifetime and cleanup.

3. Decide whether DI is needed

Use direct construction for cheap pure/local helpers and transient values that do not belong to the application object graph.

4. Make dependencies explicit

Prefer constructor injection:

final class OrdersRepository {
  OrdersRepository(this.api, this.cache);
  final OrdersApi api;
  final Cache cache;
}

Avoid business classes that resolve GetIt.I<T>() internally.

Use method parameters for call-specific data.

5. Choose lifetime

SemanticsRegistration
Fresh object each resolutionfactory
Fresh object + caller runtime argsfactory parameter
One eager/already-created objectsingleton
One shared object on first uselazy singleton
Shared async-created objectasync singleton
Shared async object only on demandlazy async singleton
Session/account/workflow lifetimescoped registration
Weak GC-driven reuse with no deterministic cleanup needcached factory, rarely

Do not choose singleton merely to avoid wiring constructors.

6. Choose manual or generated composition

Use constructors/manual get_it when the graph remains easy to read.

Use injectable when generated ordering, modules, environments, scopes, or many transitive registrations meaningfully reduce boilerplate.

Preserve a valid existing convention unless migration solves a concrete problem.

7. Handle runtime values correctly

Use factory params or ordinary constructor/method arguments for route IDs, workflow values, or call data.

Do not turn changing IDs/user input into global singleton registrations.

8. Centralize environment selection

Bind dev/staging/prod/test implementations at composition time. Avoid scattered environment branches in repositories/services.

9. Model async behavior precisely

Ask separately:

  • Does creation itself return a Future?
  • Does a created object need initialize()?
  • Must consumers wait?
  • Must runApp wait?

Prefer Future-based async registration to manual readiness signals when possible.

10. Define cleanup now

For sockets, DBs, streams, subscriptions, SDK clients, and other resources, decide who closes/disposes them before registration.

Factory-created resources are normally consumer-owned.

Composition root

Keep composition discoverable:

main()
→ bootstrap()
→ configureDependencies(environment)
→ await only critical readiness
→ runApp()

Direct locator use is acceptable here and at narrow framework creation seams.

Never register from widget build, repository methods, or opportunistic business flows.

get_it rules

Factory

Use for route/feature-local mutable objects and most Bloc/Cubit registrations. Let the requesting owner dispose them.

Lazy singleton

Use for shared services/repositories/infrastructure that need one instance but not eager startup creation. Remember the graph remains retained after first use.

Async

Use registerSingletonAsync for shared async creation.

Use dependsOn only for real dependency ordering; keep independent startup work parallel.

Use registerSingletonWithDependencies when construction is synchronous but prerequisites are async.

Resolve lazy async registrations with getAsync<T>() unless pre-resolved.

Readiness

Use allReady() only for the required async graph and consider a timeout. Use isReady<T>() when only one dependency matters.

Do not use manual signalReady where a Future can represent readiness.

Scopes

Use scopes for coherent lifetime groups:

push authenticated session
→ register UserSession + user repositories/socket/cache
→ logout/account switch
→ pop scope and await disposal

Scope teardown does not automatically revoke server tokens or erase persistent data; perform domain/security cleanup separately.

Names / multiple registrations

Use names for same-type objects that genuinely coexist, such as public and authenticated Dio.

Use environment bindings for mutually exclusive implementations.

Enable getAll<T>()-style multiple registrations only for intentional extension lists.

injectable rules

Treat annotations as registration metadata, not architecture.

  • @injectable: factory-style generated construction.
  • @singleton: intentional eager shared lifetime.
  • @lazySingleton: intentional lazy shared lifetime.
  • @module: construction of third-party/unowned dependencies.
  • as:: abstraction binding where the interface is meaningful.
  • @Named: genuine same-type coexistence.
  • environment annotations: mutually exclusive implementations.
  • @factoryMethod: non-default/static factory construction.
  • @preResolve: startup must await the Future and later synchronous resolution is needed.
  • @disposeMethod: cleanup for container-owned singleton-like objects.
  • scopes: use only when application flow still owns explicit scope entry/exit.

Regenerate after graph changes. Never hand-edit generated config.

Startup policy

Block runApp only for dependencies required to construct or safely use the application.

Often critical: backend-defining config, required encryption/database prerequisites, mandatory migration.

Often deferrable: analytics warm-up, optional cache warming, secondary SDKs, later feature data.

Render a bootstrap/loading state when non-critical initialization can occur after the Flutter tree starts.

Do not use allReady() as a universal startup hammer.

Session/account isolation

Keep app-global infrastructure separate from user-specific state.

Typical app scope:

  • environment config;
  • logger/crash reporter;
  • secure credential store;
  • public auth API/infrastructure.

Typical session scope:

  • UserSession;
  • authenticated API façade;
  • current-user repositories/caches;
  • user-specific socket/services.

On logout/account switch, stop session work, pop/await scope cleanup, clear/revoke credentials and persistent user data according to policy, then create a fresh scope for the next identity.

Bloc/Cubit integration

Typical ownership:

shared repository (lazy singleton)
→ route Cubit/Bloc (factory)
→ BlocProvider creates it
→ BlocProvider closes it

Use app-lifetime Bloc/Cubit only when state truly has application lifetime.

Do not inject GetIt into Bloc/Cubit business logic.

Infrastructure integration

Register Dio, DBs, sockets, SDKs according to actual shared lifetime.

For Dio, one backend/networking policy usually maps to one shared client; use separate named/types when base URL, auth, TLS, adapter, or lifetime materially differs.

Inject interceptor dependencies through constructors.

Do not close app-level infrastructure from a route.

Circular dependencies

Treat A → B → A as a design smell.

Do not hide the cycle using lazy GetIt.I getters. Refactor responsibility, introduce a smaller shared abstraction, or move coordination to the actual workflow owner.

Testing

Narrow unit test

Construct directly with fakes. A business class should normally be testable without initializing global GetIt.

Container test

Use GetIt.asNewInstance() for an isolated graph. Await cleanup/reset.

Use scope shadowing for bounded overrides of an existing graph.

Important: shadowing a lower dependency does not rebuild an already-created singleton that captured the old dependency. Recreate/shadow the dependent object or use a fresh graph.

Generated graph test

Use a generated test environment when testing environment/configuration behavior, not ordinary business logic.

Avoid allowReassignment as the default testing strategy because it can mask duplicate-registration mistakes.

Code generation

Use the repository's established generation command, commonly:

dart run build_runner build

Regenerate after constructor, annotation, module, scope, environment, name, binding, or lifecycle changes.

CI should detect stale generated output through the repository's normal generate/format/analyze/test workflow.

After DI graph changes during Flutter development, use hot restart when the running container must be rebuilt; hot reload usually preserves runtime state.

Performance and memory

Optimize lifetime semantics:

  • eager singleton → startup cost;
  • lazy singleton → first-use cost + retained graph;
  • factory → repeated construction but no shared retained instance;
  • app singleton → retains everything reachable from it;
  • session scope → releases user caches/sockets/subscriptions together;
  • unnecessary readiness chains → serialize startup.

Do not micro-optimize ordinary type/name lookup before these costs.

Do not use weak cached factories for resources requiring deterministic disposal.

Failure diagnosis

Not registered

Check composition init, generated freshness, binding type, environment, scope, name, and async initialization.

Duplicate registration

Check bootstrap called twice, widget/runtime registration, manual + generated duplication, overlapping environments, duplicate names, or preserved development runtime state. Fix composition before enabling reassignment.

Async not ready

Check get<T>() vs getAsync<T>(), missing awaited generated init/pre-resolution, dependsOn, manual readiness, and async cycles.

Wrong scoped implementation

Inspect active scope, shadowing, and whether session/test scopes were pushed or popped correctly.

allReady hangs

Inspect every async/readiness registration and dependency cycle. Prefer Future-based readiness over manual signaling.

Refactoring locator-heavy code

  1. Add characterization tests.
  2. Expose hidden dependencies in constructors.
  3. Update manual/generated registration.
  4. Add a direct constructor unit test.
  5. Repeat one boundary at a time.
  6. Remove legacy locator access after consumers are migrated.

Keep DI refactoring separate from unrelated domain behavior changes where possible.

Review output

Report:

Current graph/symptom
Ownership + lifetime finding
Root cause/risk
Change made/recommended
Async/readiness impact
Scope/disposal impact
Environment/name impact
Testing performed
Startup/memory impact
Residual version-sensitive risk

Production gates

Graph and direction

  • all required dependencies resolve;
  • no accidental duplicate or hidden cycle;
  • composition root is discoverable;
  • generated config matches source;
  • business classes expose constructor dependencies;
  • modules contain construction, not business workflow.

Lifetime and ownership

  • app/session/feature/route ownership is explicit;
  • route state is not accidentally singleton;
  • user-specific state ends on logout/account switch;
  • shared resources are not accidentally factories;
  • every disposable resource has a known owner.

Async/readiness

  • required initialization is deterministic;
  • startup blocks only on critical dependencies;
  • sync/async resolution APIs match registration type;
  • readiness cannot hang silently.

Environment/names

  • one explicit environment is selected at bootstrap;
  • production graph contains no fake/test binding;
  • named registrations are limited and consistent.

Testing

  • core units can be constructed directly;
  • container tests isolate and tear down state;
  • session/environment/readiness behavior is tested where material.

Performance/memory

  • expensive optional services are not eagerly created without need;
  • app-global retention is intentional;
  • session caches/sockets/subscriptions are released;
  • independent startup work is not unnecessarily serialized.

Final decision rule

Before accepting any registration, answer:

What does this object do?
Who owns it?
How long should it live?
Should consumers share it?
Does creation/readiness require await?
Who disposes it?
Does environment or runtime input change construction?
Can the consumer be unit-tested without the container?

Only then choose plain construction, factory, singleton, lazy singleton, async registration, scope, module, environment binding, or name.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.