Advanced Integration
WARNING
This content is AI generated and is currently under review.
Advanced patterns for integrating watch_it with get_it, including scopes, named instances, async initialization, and multi-package coordination.
get_it Scopes with pushScope
Scopes allow you to create temporary registrations that are automatically cleaned up when a widget is disposed. Perfect for feature-specific dependencies or screen-level state.
What are Scopes?
get_it scopes create isolated registration contexts:
- Push a scope - Create new registration context
- Register in scope - Dependencies only live in that scope
- Pop the scope - All scoped registrations are disposed
pushScope() - Automatic Scope Management
pushScope() creates a scope when the widget mounts and automatically cleans it up on dispose:
class UserProfileScreen extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Create scope on first build
pushScope(
init: (scope) {
// Register screen-specific dependencies
scope.registerLazySingleton<ProfileManager>(
() => ProfileManager(userId: '123'),
);
scope.registerLazySingleton<EditController>(
() => EditController(),
);
},
dispose: () {
// Cleanup when scope is popped (widget disposed)
print('Profile screen scope disposed');
},
);
// Use scoped dependencies
final profile = watchValue((ProfileManager m) => m.profile);
return YourUI();
}
}What happens:
- Widget builds first time → Scope is pushed,
initcallback runs - Dependencies registered in new scope
- Widget can watch scoped dependencies
- Widget disposes → Scope is automatically popped,
disposecallback runs - All scoped registrations are cleaned up
Use Cases for Scopes
1. Screen-Specific State
class ProductDetailScreen extends WatchingWidget {
final String productId;
ProductDetailScreen({required this.productId});
@override
Widget build(BuildContext context) {
pushScope(
init: (scope) {
// Register screen-specific manager
scope.registerLazySingleton<ProductDetailManager>(
() => ProductDetailManager(productId: productId),
);
},
);
final product = watchValue((ProductDetailManager m) => m.product);
final isLoading = watchValue((ProductDetailManager m) => m.isLoading);
if (isLoading) return CircularProgressIndicator();
return ProductDetailView(product: product);
}
}2. Feature Modules
class CheckoutFlow extends WatchingWidget {
@override
Widget build(BuildContext context) {
pushScope(
init: (scope) {
// Register all checkout-related services
scope.registerLazySingleton<CartManager>(() => CartManager());
scope.registerLazySingleton<PaymentService>(() => PaymentService());
scope.registerLazySingleton<ShippingService>(() => ShippingService());
},
dispose: () {
print('Checkout flow completed, cleaning up');
},
);
return CheckoutStepper();
}
}3. User Session State
class AuthenticatedApp extends WatchingWidget {
final User user;
AuthenticatedApp({required this.user});
@override
Widget build(BuildContext context) {
pushScope(
init: (scope) {
// User-specific services
scope.registerLazySingleton<AdvancedUserManager>(
() => AdvancedUserManager(user),
);
scope.registerLazySingleton<UserPreferences>(
() => UserPreferences(userId: user.id),
);
},
dispose: () {
// User logged out, clean up user-specific state
print('User session ended');
},
);
return HomeScreen();
}
}Scope Best Practices
✅ DO:
- Use scopes for screen/feature-specific dependencies
- Clean up resources in
disposecallback - Keep scopes focused and short-lived
❌️ DON'T:
- Use scopes for app-wide singletons (use global registration)
- Create deeply nested scopes (keeps things simple)
- Register the same type in multiple scopes (use named instances instead)
Named Instances
Watch specific named instances from get_it:
Registering Named Instances
void setupDependencies() {
// Register multiple instances of same type with different names
di.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: 'https://api.prod.com'),
instanceName: 'production',
);
di.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: 'https://api.staging.com'),
instanceName: 'staging',
);
di.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: 'http://localhost:3000'),
instanceName: 'development',
);
}Watching Named Instances
class ApiMonitor extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch specific named instances
final prodApi = watchValue(
(ApiClientExtended api) => api.requestCount,
instanceName: 'production',
);
final stagingApi = watchValue(
(ApiClientExtended api) => api.requestCount,
instanceName: 'staging',
);
return Column(
children: [
Text('Production: $prodApi requests'),
Text('Staging: $stagingApi requests'),
],
);
}
}Environment-Specific Configuration
class AppConfig {
static const environment =
String.fromEnvironment('ENV', defaultValue: 'development');
static void setup() {
di.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: _getBaseUrl()),
instanceName: environment,
);
}
static String _getBaseUrl() {
switch (environment) {
case 'production':
return 'https://api.prod.com';
case 'staging':
return 'https://api.staging.com';
default:
return 'http://localhost:3000';
}
}
}
// Usage
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue(
(ApiClientExtended api) => api.data,
instanceName: AppConfig.environment,
);
return DataDisplay(data);
}
}Async Initialization with isReady and allReady
Handle complex initialization scenarios where multiple async dependencies must be ready before the app starts.
isReady - Single Dependency
Check if a specific async dependency is ready:
void setupDependenciesAsync() async {
// Register async singleton
di.registerSingletonAsync<Database>(
() async {
final db = Database();
await db.initialize();
return db;
},
);
}
class App extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Check if ready
final ready = isReady<Database>();
if (!ready) {
return SplashScreen();
}
return MainApp();
}
}allReady - Multiple Dependencies
Wait for all async dependencies to complete:
void setupMultipleDependencies() async {
di.registerSingletonAsync<Database>(() async {
final db = Database();
await db.initialize();
return db;
});
di.registerSingletonAsync<ConfigService>(() async {
final config = ConfigService();
await config.loadFromFile();
return config;
});
di.registerSingletonAsync<AuthServiceAdvanced>(
() async {
final auth = AuthServiceAdvanced();
await auth.initialize();
return auth;
},
dependsOn: [Database], // Waits for Database first
);
}
class AppAllReady extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Wait for all async singletons
final ready = allReady(
timeout: Duration(seconds: 30),
);
if (!ready) {
return SplashScreen();
}
return MainApp();
}
}Watching Initialization Progress
class InitializationScreen extends WatchingWidget {
@override
Widget build(BuildContext context) {
final dbReady = isReady<Database>();
final configReady = isReady<ConfigService>();
final authReady = isReady<AuthServiceAdvanced>();
final progress =
[dbReady, configReady, authReady].where((ready) => ready).length / 3;
if (dbReady && configReady && authReady) {
// All ready, navigate to main app
callOnce((_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => MainApp()),
);
});
}
return Column(
children: [
LinearProgressIndicator(value: progress),
Text('Initializing... ${(progress * 100).toInt()}%'),
if (dbReady) Text('✓ Database ready'),
if (configReady) Text('✓ Configuration loaded'),
if (authReady) Text('✓ Authentication ready'),
],
);
}
}Custom GetIt Instances
Use multiple GetIt instances for different contexts:
// Global app dependencies
final appDI = GetIt.instance;
// Test-specific dependencies
final testDI = GetIt.asNewInstance();
// Feature module dependencies
final featureDI = GetIt.asNewInstance();
// Setup
void setupApp() {
appDI.registerLazySingleton<ApiClient>(() => ApiClient());
appDI.registerLazySingleton<Database>(() => Database());
}
void setupFeature() {
featureDI.registerLazySingleton<FeatureManager>(() => FeatureManager());
}
// Usage in widgets
class FeatureWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch from specific GetIt instance
final data = watchValue(
(FeatureManager m) => m.data,
getIt: featureDI, // Use feature-specific instance
);
return YourUI();
}
}Multi-Package Integration
Coordinate watch_it across multiple packages in a monorepo or modular app.
Package Structure
app/
├── core_package/
│ └── lib/
│ └── managers/
│ └── auth_manager.dart
├── feature_a/
│ └── lib/
│ └── managers/
│ └── feature_a_manager.dart
└── main_app/
└── lib/
└── main.dartCore Package Setup
// core_package/lib/core_package.dart
// export 'managers/auth_manager.dart';
class CorePackageExample {
static void register(GetIt di) {
di.registerLazySingleton<AuthManager>(() => AuthManager());
di.registerLazySingleton<ApiClient>(() => ApiClient());
}
}Feature Package Setup
// feature_a/lib/feature_a.dart
// export 'managers/feature_a_manager.dart';
class FeatureAExample {
static void register(GetIt di) {
// Depends on CorePackage being registered first
di.registerLazySingleton<AdvancedFeatureAManager>(
() => AdvancedFeatureAManager(
auth: di<AuthManager>(), // From core package
api: di<ApiClient>(),
),
);
}
}Main App Integration
// main_app/lib/main.dart
// import 'package:core_package/core_package.dart';
// import 'package:feature_a/feature_a.dart';
void mainExample() {
// Register all packages
CorePackage.register(di);
FeatureA.register(di);
runApp(MyApp());
}
class FeatureAScreen extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Can watch dependencies from any package
final user = watchValue((AuthManager m) => m.user); // From core
final data =
watchValue((AdvancedFeatureAManager m) => m.data); // From feature_a
return YourUI();
}
}Package Registration Order
void setupDependenciesOrder() {
// Order matters for dependencies!
// 1. Core dependencies first
CorePackage.register(di);
// 2. Feature packages (depend on core)
FeatureA.register(di);
FeatureB.register(di);
// 3. App-level dependencies (depend on everything)
AppPackage.register(di);
}Integration Patterns
Pattern 1: Lazy Module Loading
class FeatureLoader extends WatchingWidget {
@override
Widget build(BuildContext context) {
callOnce((_) {
// Load feature module on demand
pushScope(
init: (scope) {
FeatureModule.register(scope);
},
);
});
final ready = isReady<FeatureManager>();
if (!ready) return CircularProgressIndicator();
return FeatureUI();
}
}Pattern 2: A/B Testing with Named Instances
void setupABTest() {
final variant = Random().nextBool() ? 'A' : 'B';
di.registerLazySingleton<CheckoutFlowBase>(
() => variant == 'A' ? CheckoutFlowA() : CheckoutFlowB(),
instanceName: 'current',
);
}
class CheckoutScreen extends WatchingWidget {
@override
Widget build(BuildContext context) {
final flow = watchIt<CheckoutFlowBase>(
instanceName: 'current',
);
return flow.buildUI();
}
}Pattern 3: Hot Swap Dependencies
class DebugPanel extends WatchingWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// Unregister old
di.unregister<ApiClient>();
// Register new
di.registerLazySingleton<ApiClient>(
() => MockApiClient(), // Swap to mock
);
// Widgets watching ApiClient will rebuild
},
child: Text('Switch to Mock API'),
);
}
}Advanced Patterns
Local Reactive State with createOnce and watch
For widget-local reactive state that doesn't need get_it registration, combine createOnce with watch:
class CounterWidget extends WatchingWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
// Create a local notifier that persists across rebuilds
final counter = createOnce(() => ValueNotifier<int>(0));
// Watch it directly (not from get_it)
final count = watch(counter).value;
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
}
}When to use this pattern:
- Widget needs its own local reactive state
- State should persist across rebuilds (not recreated)
- State should be automatically disposed with widget
- Don't want to register in get_it (truly local)
Key benefits:
createOncecreates the notifier once and auto-disposes itwatchsubscribes to changes and triggers rebuilds- No manual lifecycle management needed
Global State Reset
class AppResetButton extends WatchingWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
// Reset all scopes
await di.reset();
// Re-register dependencies
setupDependencies();
// Navigate to fresh start
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => LoginScreen()),
(route) => false,
);
},
child: Text('Reset App'),
);
}
}Dependency Injection Testing
void main() {
// Test setup example
void testSetup() {
// Reset get_it before each test
di.reset();
// Register mocks
di.registerLazySingleton<AuthManager>(
() => MockAuthManager(),
);
}
// Example test structure (not actual test)
testSetup();
}See Also
- get_it Scopes Documentation - Detailed scope information
- get_it Async Objects - Async initialization
- Best Practices - General best practices
- Testing - Testing with get_it