Dire DI Flutter
A Spring-like dependency injection framework for Dart and Flutter with code generation support for mobile platforms.
Features
- Spring-like Annotations:
@Service,@Repository,@Component,@Controller,@DataSource,@UseCase - Constructor Injection: Modern dependency injection via constructors (recommended)
- Code Generation: Flutter-compatible using build_runner instead of dart:mirrors
- Consolidated Generation:
@DireDiEntryPointfor single-file registration - Multi-File Support: Components spread across multiple files automatically discovered
- BLoC Pattern Support: Full compatibility with part files for state management
- AutoRoute Integration: Built-in router service for type-safe navigation
- Flutter Mixin: Easy integration with StatefulWidget via
DiCoreandDiMixin - Qualifier Support: Use named instances for specific bean selection
- Singleton and Prototype Scopes: Control object lifecycle
- Conditional Registration:
@ConditionalOnProperty,@ConditionalOnClass - Profile Support:
@Profilefor environment-specific beans - Configuration Classes:
@Configurationand@Beanfor manual setup - Smart File Processing: Automatic filtering of generated and part files
Installation
Add this to your package's pubspec.yaml file:
dependencies:
dire_di_flutter: ^2.5.0
dev_dependencies:
build_runner: ^2.4.13
Quick Start
1. Define Your Services (Constructor Injection - Recommended)
import 'package:dire_di/dire_di.dart';
@Service()
class UserService {
final UserRepository userRepository;
final EmailService emailService;
UserService(this.userRepository, this.emailService);
Future<User> getUser(int id) {
return userRepository.findById(id);
}
}
@Repository()
class UserRepository {
Future<User> findById(int id) async {
// Database access logic here
return User(id: id, name: 'John Doe');
}
}
@Service()
class EmailService {
void sendEmail(String to, String subject, String body) {
print('Sending email to $to: $subject');
}
}
2. Initialize the Container
void main() async {
final container = DireContainer();
await container.scan(); // Auto-discover and register components
final userService = container.get<UserService>();
final user = await userService.getUser(1);
print('User: ${user.name}');
}
3. Run Code Generation
dart pub run build_runner build
Note: The package uses the
dire_di_generatorbuilder by default, which is mirrors-free and mobile-compatible. Legacy builders are available as opt-in only to avoid conflicts.
Consolidated Generation (New in v2.0.0)
For larger projects with components spread across multiple files, use @DireDiEntryPoint to consolidate all registrations into a single file:
1. Create an Entry Point
// app_module.dart
import 'package:dire_di_flutter/dire_di.dart';
import 'app_module.dire_di.dart'; // Generated file
@DireDiEntryPoint()
class AppModule {
// Entry point for DI configuration
}
2. Spread Components Across Files
// services/user_service.dart
@Service()
class UserService {
@Autowired()
late UserRepository userRepository;
}
// repositories/user_repository.dart
@Repository()
class UserRepository {
// Implementation
}
// controllers/user_controller.dart
@Controller()
class UserController {
@Autowired()
late UserService userService;
}
3. Single Registration Call
void main() async {
final container = DireContainer();
await container.scan();
// All components from across the project registered with one call!
container.registerGeneratedDependencies();
final controller = container.get<UserController>();
}
Benefits:
- ✅ Only one
registerGeneratedDependencies()call needed - ✅ Components automatically discovered across all files
- ✅ Single consolidated
.dire_di.dartfile generated - ✅ Proper dependency ordering maintained
- ✅ Better organization for large projects
Flutter Integration with DiCore
For easier Flutter integration, use the DiCore mixin with your StatefulWidget states:
⚠️ Important: Async Initialization Required
The DI container initialization is asynchronous. You must handle this properly to avoid exceptions:
Option 1: Pre-initialize in main() (Recommended)
import 'package:flutter/material.dart';
import 'package:dire_di_flutter/dire_di.dart';
import 'app_module.dire_di.dart'; // Your generated file
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// CRITICAL: Pre-initialize DI container before runApp()
await DiCore.initialize();
// Register generated dependencies
final container = DireContainer();
await container.scan();
container.registerGeneratedDependencies();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with DiCore, DiMixin {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
Option 2: Async Pattern in Widgets
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with DiCore, DiMixin {
UserService? userService;
bool isLoading = true;
@override
void initState() {
super.initState();
_loadDependencies();
}
void _loadDependencies() async {
try {
// Use convenience properties from DiMixin (recommended)
userService = await userServiceAsync;
// Or use direct async access:
// userService = await getAsync<UserService>();
setState(() {
isLoading = false;
});
} catch (e) {
print('DI Error: $e');
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Text('User: ${userService?.getCurrentUser() ?? "None"}'),
),
);
}
}
❌ Common Mistakes to Avoid
// DON'T DO THIS - Will cause exceptions
class _BadExampleState extends State<BadExample> with DiCore {
late UserService userService;
@override
void initState() {
super.initState();
userService = get<UserService>(); // ❌ This will throw!
}
}
// ✅ DO THIS INSTEAD
class _GoodExampleState extends State<GoodExample> with DiCore, DiMixin {
UserService? userService;
@override
void initState() {
super.initState();
_initAsync();
}
void _initAsync() async {
userService = await userServiceAsync; // ✅ Safe async access
setState(() {});
}
}
DiMixin Convenience Properties
When you use DiMixin, you get auto-generated convenience properties for all your dependencies:
class _MyWidgetState extends State<MyWidget> with DiCore, DiMixin {
void someMethod() async {
// Auto-generated properties from DiMixin:
final user = await userServiceAsync;
final repo = await userRepositoryAsync;
final controller = await userControllerAsync;
// These are equivalent to:
// final user = await getAsync<UserService>();
}
}
Mixin API
// Async methods (recommended - auto-initialize container)
final service = await getAsync<UserService>();
final namedService = await getAsync<ApiClient>('prod');
final hasService = await hasAsync<UserService>();
final allServices = await getAllAsync<UserService>();
// Sync methods (container must be pre-initialized)
final service = get<UserService>();
final namedService = get<ApiClient>('prod');
final hasService = has<UserService>();
final allServices = getAll<UserService>();
// Manual registration
await register<ApiClient>(() => ApiClient(baseUrl: 'https://api.example.com'));
// Container management
await DireDiMixin.initialize(); // Pre-initialize
DireDiMixin.reset(); // Reset for testing
Benefits of DireDiMixin
- ✅ No Manual Container Management: Automatic DireContainer setup
- ✅ Global Container: Single instance shared across all widgets
- ✅ Sync and Async Support: Choose based on your initialization needs
- ✅ Type Safety: Full generic type support with compile-time checking
- ✅ Qualifier Support: Named instances via
get<T>('name') - ✅ Convenient Methods:
has<T>(),getAll<T>(),register<T>() - ✅ Flutter Optimized: Designed specifically for StatefulWidget states
Auto-Generated Convenience Properties
For even easier access, the generated .dire_di.dart file includes a DI mixin with direct property access:
Direct Property Access
class _MyWidgetState extends State<MyWidget> with DireDiMixin, DI {
@override
Widget build(BuildContext context) {
// Direct property access - no get<T>() calls needed!
return Column(
children: [
Text('User: ${userService.getCurrentUser()}'),
Text('Database: ${databaseService.isConnected()}'),
Text('Config: ${configurationService.getConfigValue('app.name')}'),
],
);
}
void updateUser() {
// Direct controller access
userController.updateUser('New Name');
}
}
Async Property Access
class _AsyncWidgetState extends State<AsyncWidget> with DireDiMixin, DI {
String? data;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
// Async properties ensure safe initialization
final svc = await userServiceAsync;
final db = await databaseServiceAsync;
setState(() {
data = '${svc.getCurrentUser()} from ${db.getDatabaseUrl()}';
});
}
}
Auto-Generated Properties
For each @Service(), @Repository(), @Component(), etc., the generator creates:
// If you have UserService, you get:
UserService get userService;
Future<UserService> get userServiceAsync;
// If you have DatabaseService, you get:
DatabaseService get databaseService;
Future<DatabaseService> get databaseServiceAsync;
// And so on for all your components...
Benefits of Convenience Properties
- ✅ Zero Boilerplate: No manual property definitions
- ✅ Direct Access:
userServiceinstead ofget<UserService>() - ✅ Type Safe: Full compile-time type checking
- ✅ IDE Friendly: Auto-completion and refactoring support
- ✅ Auto-Generated: Automatically updated when you add/remove services
- ✅ Clean Code: Eliminates DI syntax completely from UI code
Mobile Platform Support
This package fully supports Android, iOS, and all Flutter platforms through mirrors-free code generation.
Why No dart:mirrors Issues?
Unlike some DI packages, dire_di_flutter uses code generation instead of runtime reflection:
- Build Time: Code analysis happens during
dart pub run build_runner build - Runtime: Generated code contains zero mirrors dependency
- Mobile Compatible: Works on all Flutter platforms without restrictions
How It Works
// 1. Your annotated classes (any platform)
@Service()
class UserService { ... }
// 2. Code generation creates (mirrors-free)
extension GeneratedDependencies on DireContainer {
void registerGeneratedDependencies() {
register<UserService>(() => UserService());
}
}
// 3. Your app uses plain Dart code (any platform)
container.registerGeneratedDependencies(); // ✅ Works everywhere
The Magic: The build process uses advanced static analysis (not mirrors) to discover your components and generates plain Dart registration code that runs anywhere Flutter does.
Advanced Usage
BLoC with Part Files
The framework supports BLoC patterns that use part files for states and events:
// user_bloc.dart - Main file with DI annotation
@Controller()
class UserBloc extends Bloc<UserEvent, UserState> {
@Autowired()
final UserRepository userRepository;
UserBloc({required this.userRepository}) : super(UserInitial());
}
// user_state.dart - Part file (automatically skipped)
part of 'user_bloc.dart';
class UserState {}
// user_event.dart - Part file (automatically skipped)
part of 'user_bloc.dart';
class UserEvent {}
AutoRoute Integration
Make navigation services injectable:
@Service()
class RouterService {
final AppRouter _router = AppRouter();
AppRouter get router => _router;
void navigateToHome() => _router.push(const HomeRoute());
}
Qualifiers
When you have multiple implementations:
@Service()
@Qualifier('primary')
class PrimaryUserService implements UserService {}
@Service()
@Qualifier('secondary')
class SecondaryUserService implements UserService {}
// Inject specific implementation
@Autowired()
@Qualifier('primary')
UserService primaryService;
Configuration Classes
@Configuration()
class AppConfig {
@Bean()
Database createDatabase() {
return Database(connectionString: 'sqlite://app.db');
}
}
Profiles
@Service()
@Profile('development')
class DevEmailService implements EmailService {}
@Service()
@Profile('production')
class ProdEmailService implements EmailService {}
// Only registered if sqflite package is available }
## Troubleshooting
### Common Issues and Solutions
#### 1. Build Errors with Part Files
**Problem**: Getting "Asset is not a Dart library" errors during build.
**Solution**: Dire DI automatically excludes part files and generated files. Ensure your part files are properly declared:
```dart
// ✅ Correct - Part file properly excluded
part of 'user_bloc.dart';
// ❌ Incorrect - Missing part declaration
// This file will be processed and may cause errors
2. BLoC with Part Files Not Injecting
Problem: BLoC using part files for states/events isn't being registered.
Solution: Keep DI annotations in the main BLoC file, not part files:
// user_bloc.dart - Main file with DI annotations
@Controller() // ✅ Annotation here
class UserBloc extends Bloc<UserEvent, UserState> {
@Autowired()
final UserRepository repository;
}
// user_state.dart - Part file (no annotations needed)
part of 'user_bloc.dart';
// ✅ Just part file content, no DI annotations
3. Generated Files Causing Build Issues
Problem: Code generation conflicts with Dire DI processing.
Solution: Run build commands in the correct order:
# Clean first
flutter packages pub run build_runner clean
# Generate Dire DI files
flutter packages pub run build_runner build --filter="dire_di"
# Then generate other files (freezed, json, etc.)
flutter packages pub run build_runner build
4. AutoRoute Not Working with DI
Problem: Navigation not working after DI integration.
Solution: Ensure RouterService is properly configured and injected:
// ✅ Register RouterService as singleton
@Service()
class RouterService {
static final _instance = AppRouter();
AppRouter get router => _instance;
}
// ✅ Use RouterService in MaterialApp
final routerService = get<RouterService>();
MaterialApp.router(routerConfig: routerService.router.config())
Comparison with Other DI Solutions
vs get_it + injectable
get_it + injectable:
@injectable
class UserService {
UserService(this.userRepository);
final UserRepository userRepository;
}
// Need manual to direct access
// Hard to read
dire_di_flutter:
@Service()
class UserService {
@Autowired()
late UserRepository userRepository; // Field injection!
}
// Direct access via mixin
// Spring-like annotations
Key Advantages
- Spring-like field injection with @Autowired annotation
- Code generation for mobile platform compatibility (no dart:mirrors at runtime)
- Automatic component scanning and discovery
- Spring-familiar annotations (@Service, @Repository, @Controller)
- Rich conditional and profile support
- Flutter integration via DiCore and DiMixin
Troubleshooting
Part Files Build Errors
If you get "Asset is not a Dart library" errors, ensure part files have proper declarations:
part of 'user_bloc.dart'; // Required at top of part files
BLoC Not Being Injected
Keep DI annotations in the main BLoC file, not in part files:
@Controller() // In main user_bloc.dart file
class UserBloc extends Bloc<UserEvent, UserState> {}
Build Order Issues
Run code generation in this order:
flutter packages pub run build_runner clean
flutter packages pub run build_runner build --filter="dire_di"
flutter packages pub run build_runner build
API Reference
Core Annotations
@Component()- Base component@Service()- Service layer@Repository()- Data access layer@Controller()- Presentation layer@DataSource()- Data source layer@UseCase()- Use case layer@Autowired()- Dependency injection@Qualifier(name)- Bean qualification@Singleton()- Single instance@Configuration()- Config classes@Bean()- Factory methods
Container Methods
final container = DireContainer();
await container.scan();
final service = container.get<UserService>();
bool exists = container.contains<UserService>();
container.register<UserService>(() => UserService());
Best Practices
- Use specific annotations like
@Service,@Repositoryover@Component - Add
@Qualifierwhen you have multiple implementations - Use
@Profilefor environment-specific configurations - Use constructor injection when possible, field injection with @Autowired for convenience
- Pre-initialize DI container in main() for Flutter apps
Contributing
Contributions welcome! Please submit a Pull Request.
License
MIT License - see LICENSE file for details.