dire_di_flutter 2.5.0
dire_di_flutter: ^2.5.0 copied to clipboard
A Spring-like dependency injection framework for Dart with code generation for Flutter compatibility.
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:
@DireDiEntryPoint
for 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
DiCore
andDiMixin
- Qualifier Support: Use named instances for specific bean selection
- Singleton and Prototype Scopes: Control object lifecycle
- Conditional Registration:
@ConditionalOnProperty
,@ConditionalOnClass
- Profile Support:
@Profile
for environment-specific beans - Configuration Classes:
@Configuration
and@Bean
for 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_generator
builder 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.dart
file 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:
userService
instead 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
,@Repository
over@Component
- Add
@Qualifier
when you have multiple implementations - Use
@Profile
for 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.