Quickstart
This chapter builds a counter app the way flutter_demo does —
a small, layered Flutter app that follows Flutter’s recommended
architecture. Every snippet is taken from that example; open the
linked files alongside this chapter for the complete, running source.
By the end you will know how to:
- add inject.dart to a Flutter project
- annotate classes and provide third-party types with a module
- expose runtime-parameterised widgets with
@assistedInject - declare a component and run the generator
- use the wired-up component in
main()
Create the project
flutter create counter_app
cd counter_app
flutter pub add inject_annotation inject_flutter dev:inject_generator dev:build_runner
(Chapter 2 covers the packages and their roles in detail.)
The concepts
inject.dart wires dependencies through a handful of annotations:
| Concept | Annotation | What it means |
|---|---|---|
| Injectable | @inject | The generator constructs this class via its constructor |
| Module | @module/@provides | Provides types you don’t own (third-party, interfaces) |
| Singleton | @singleton | One shared instance for the component’s lifetime |
| Assisted | @assistedInject | Mixes graph-injected and runtime parameters |
| Component | @Component | Root of the graph; exposes entry points |
Everything starts from a @Component. The generator traces the graph from its
entry points and resolves every dependency transitively — at build time.
The files
The app is split by layer, mirroring flutter_demo:
lib/
├── main.dart @Component root (MainComponent)
└── src/
├── app_module.dart AppModule (initial count, …)
├── domain/
│ ├── models/counter.dart Counter (immutable domain model)
│ └── use_cases/increment_counter_use_case.dart IncrementCounterUseCase
├── data/
│ ├── services/database.dart Database + DatabaseModule
│ └── repositories/counter_repository.dart CounterRepository
└── features/
├── app/my_app.dart MyApp (@assistedInject)
└── home/
├── home_page.dart HomePage (@assistedInject)
└── counter_view_model.dart CounterViewModel
Step 1 — Provide a third-party type with a module
Database stands in for a third-party library (Drift, Hive, Isar, …). It can’t
carry @inject, so a @module binds it. The module also shows the two-line
@Qualifier form to distinguish two String config values of the same Dart
type (more in Core Concepts):
// lib/src/data/services/database.dart
import 'package:inject_annotation/inject_annotation.dart';
const databasePath = Qualifier(#databasePath);
const databaseName = Qualifier(#databaseName);
@module
class DatabaseModule {
@provides
@databasePath
String provideDatabasePath() => '/data/counter.db';
@provides
@databaseName
String provideDatabaseName() => 'counter_db';
@provides
@singleton
Database provideDatabase(
@databasePath String path,
@databaseName String name,
) => Database(path: path, name: name);
}
class Database {
Database({required this.path, required this.name});
final String path;
final String name;
int _count = 0;
Future<void> updateCount(int count) async => _count = count;
Future<int> selectCount() => Future.value(_count);
}
@provides tells the generator how to build a Database; @singleton shares
one instance across the graph. Full source: database.dart.
Step 2 — Annotate your own classes with @inject
Counter is a plain immutable domain model — no annotation needed:
// lib/src/domain/models/counter.dart
class Counter {
const Counter({this.value = 0});
final int value;
Counter copyWith({int? value}) => Counter(value: value ?? this.value);
}
The repository wraps the service and returns domain models; the use case
encapsulates one operation. Both are stateless, so both are @singleton:
// lib/src/data/repositories/counter_repository.dart
@inject
@singleton
class CounterRepository {
const CounterRepository({required this._database});
final Database _database;
Future<Counter> get counter async =>
Counter(value: await _database.selectCount());
Future<void> increment() async {
final current = await _database.selectCount();
await _database.updateCount(current + 1);
}
}
// lib/src/domain/use_cases/increment_counter_use_case.dart
@inject
@singleton
class IncrementCounterUseCase {
const IncrementCounterUseCase({required this._repository});
final CounterRepository _repository;
Future<Counter> execute() async {
await _repository.increment();
return _repository.counter;
}
}
@inject tells the generator to construct the class via its constructor,
resolving each parameter from the graph. Full source:
counter_repository.dart, increment_counter_use_case.dart.
Step 3 — The ViewModel
The ViewModel holds the mutable UI state and extends ChangeNotifier. It is
not a singleton — each screen gets its own:
// lib/src/features/home/counter_view_model.dart
@inject
class CounterViewModel extends ChangeNotifier {
CounterViewModel({
required this._incrementUseCase,
required this._initialCount,
});
final IncrementCounterUseCase _incrementUseCase;
final Future<int> _initialCount;
Counter _counter = const Counter();
Counter get counter => _counter;
Future<void> init() async {
_counter = Counter(value: await _initialCount);
notifyListeners();
}
Future<void> increment() async {
_counter = await _incrementUseCase.execute();
notifyListeners();
}
}
_initialCount is a raw Future<int> binding (no @asynchronous) — the
ViewModel awaits it itself in init(). This keeps the ViewModel’s dependency
chain synchronous, which ViewModelFactory requires (the two async patterns
are explained in Core Concepts).
Full source: counter_view_model.dart.
Step 4 — Widgets with @assistedInject
A widget needs DI-managed dependencies (a ViewModelFactory) and runtime
parameters (key, title). @assistedInject mixes the two. Each file that
declares an @assistedInject constructor needs its own
part '<file>.factory.dart'; directive so the factory builder can write the
generated factory next to it:
// lib/src/features/home/home_page.dart
import 'package:flutter/material.dart';
import 'package:inject_annotation/inject_annotation.dart';
import 'package:inject_flutter/inject_flutter.dart';
import 'counter_view_model.dart';
part 'home_page.factory.dart';
class HomePage extends StatelessWidget {
@assistedInject
const HomePage({
@assisted super.key,
@assisted required this.title,
required this.viewModelFactory,
});
final String title;
final ViewModelFactory<CounterViewModel> viewModelFactory;
@override
Widget build(BuildContext context) {
return viewModelFactory(
init: (vm) => vm.init(),
loading: const Center(child: CircularProgressIndicator()),
builder: (context, vm, _) => Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('${vm.counter.value}')),
floatingActionButton: FloatingActionButton(
onPressed: vm.increment,
child: const Icon(Icons.add),
),
),
);
}
}
@assistedparameters (key,title) come from the caller at runtime.- Parameters without
@assisted(viewModelFactory) come from the graph. - The generator synthesises a
HomePageFactoryintohome_page.factory.dart— you never write it. ViewModelFactory<CounterViewModel>(frominject_flutter) creates the VM ininitState, runs theinit:callback you pass once (awaiting it when async and showingloading:meanwhile), rebuilds the subtree onnotifyListeners(), and disposes the VM indispose. Passinginit: (vm) => vm.init()is what runsinit()— the factory does not call it automatically.
The root MyApp widget follows the same pattern — its own file, its own part
directive, injecting the synthesised HomePageFactory:
// lib/src/features/app/my_app.dart
import 'package:flutter/material.dart';
import 'package:inject_annotation/inject_annotation.dart';
import '../home/home_page.dart';
part 'my_app.factory.dart';
class MyApp extends StatelessWidget {
@assistedInject
const MyApp({@assisted super.key, required this.homePageFactory});
final HomePageFactory homePageFactory;
@override
Widget build(BuildContext context) => MaterialApp(
home: homePageFactory.create(title: 'Flutter Counter Demo'),
);
}
Full source: home_page.dart, my_app.dart.
Step 5 — A module for the initial count
CounterViewModel needs a Future<int> for its initial value. A small
AppModule provides it:
// lib/src/app_module.dart
@module
class AppModule {
@provides
@singleton
Future<int> provideInitialCount() => Future.value(0);
}
In
flutter_demo,AppModulealso wires app metadata (an@asynchronousAppInfo), a@welcomemessage, and a@provisionListener— seeapp_module.dartand Core Concepts.
Step 6 — Declare the component
The component is the graph root. It lists its modules and exposes entry points.
A component is an abstract graph declaration — it never declares constructors
of its own. Since no class in main.dart declares an @assistedInject
constructor, the file needs no part directive; it imports the generated
component library instead:
// lib/main.dart
import 'main.inject.dart' as g;
@Component([AppModule, DatabaseModule])
abstract class MainComponent {
static const create = g.MainComponent$Component.create;
@inject
MyAppFactory get myAppFactory;
// flutter_demo also exposes welcomeMessage, counterRepositoryProvider,
// and creationLogListener — see main.dart.
}
import 'main.inject.dart' as g;— the generated component class lives here; theas gprefix keeps generated names out of your namespace.- Module order matters: a later module overrides an earlier one for the same
(type, qualifier)key.
Step 7 — Run the generator
dart run build_runner build
The generator writes, next to each source file:
lib/main.inject.dart— the component implementation (next tomain.dart).lib/src/features/home/home_page.factory.dart—HomePageFactory.lib/src/features/app/my_app.factory.dart—MyAppFactory.
A .factory.dart is emitted only for files that declare @assistedInject
(or @assistedFactory). main.dart has neither, so there is no
main.factory.dart.
Step 8 — Use the component in main()
void main() {
final component = MainComponent.create();
runApp(component.myAppFactory.create());
}
MainComponent.create() returns the wired component; myAppFactory.create()
produces a MyApp with every injected dependency already in place.
Troubleshooting
“Component ‘MainComponent’ has no entry points”
A @Component must expose at least one entry point — an abstract getter or
method (here, myAppFactory). Add one if your component has none.
“Could not find a way to provide X”
The generator has no binding for a dependency. Common causes:
- the class is missing
@inject; - it’s a third-party type with no
@module+@provides; - the module is not listed in
@Component([...]); - a
@Qualifieron the consumer matches no provider.
Missing part '<file>.factory.dart'; directive
A file with an @assistedInject (or @assistedFactory) constructor needs a
part '<file>.factory.dart'; directive in that same file — for example
part 'home_page.factory.dart'; in home_page.dart. It is not added to the
component file unless another class in that same file declares an assisted
constructor (the component itself never does).
Complete example
The full, running source is in flutter_demo. For a single-file
variant, see examples/example/lib/main.dart. The deeper
architecture walkthrough is in
State Management and Application Architecture.