Scopes
Los scopes proporcionan gestión jerárquica del ciclo de vida para tus objetos de negocio, independiente del árbol de widgets.
Scopes de get_it vs Scoping del Árbol de Widgets (Provider, InheritedWidget)
Los scopes de get_it son intencionalmente independientes del árbol de widgets. Gestionan el ciclo de vida de objetos de negocio basándose en el estado de la aplicación (login/logout, sesiones, características), no en la posición del widget.
Para scoping del tiempo de vida del widget, usa pushScope de watch_it que automáticamente empuja un scope de get_it durante el tiempo de vida de un widget.
¿Qué son los Scopes?
Piensa en los scopes como una pila de capas de registro. Cuando registras un tipo en un nuevo scope, este oculta (sombrea) cualquier registro del mismo tipo en scopes inferiores. Hacer pop de un scope automáticamente restaura los registros anteriores y limpia los recursos.
Cómo Funciona el Shadowing
// Base scope
getIt.registerSingleton<User>(GuestUser());
// Push new scope
getIt.pushNewScope(scopeName: 'logged-in');
getIt.registerSingleton<User>(LoggedInUser());
getIt<User>(); // Returns LoggedInUser (shadows GuestUser)
// Pop scope
await getIt.popScope();
getIt<User>(); // Returns GuestUser (automatically restored)El orden de búsqueda es de arriba hacia abajo - get_it siempre devuelve la primera coincidencia empezando desde el scope actual.
Cuándo Usar Scopes
✅ Casos de Uso Perfectos
1. Estados de Autenticación
void main() async {
final token = 'auth_token_123';
final user = GuestUser();
// App startup - guest mode
getIt.registerSingleton<User>(GuestUser());
getIt.registerSingleton<Permissions>(GuestPermissions());
// User logs in
getIt.pushNewScope(scopeName: 'authenticated');
getIt.registerSingleton<User>(AuthenticatedUser(token));
getIt.registerSingleton<Permissions>(UserPermissions(user));
// User logs out - automatic cleanup
await getIt.popScope(); // GuestUser & GuestPermissions restored
}2. Gestión de Sesiones
// Start new shopping session
getIt.pushNewScope(scopeName: 'session');
getIt.registerSingleton<ShoppingCart>(ShoppingCart());
getIt.registerSingleton<SessionAnalytics>(SessionAnalytics());
// End session - cart discarded, analytics sent
await getIt.popScope();3. Feature Flags / Pruebas A-B
const featureFlagEnabled = true;
// Setup base scope with original checkout
final userService = await UserService.load();
getIt.registerSingleton<CheckoutService>(CheckoutService(userService));
if (featureFlagEnabled) {
getIt.pushNewScope(scopeName: 'feature-new-checkout');
getIt.registerSingleton<CheckoutService>(NewCheckoutService(userService));
} else {
// Uses base scope's original CheckoutService
}4. Aislamiento de Pruebas
setUp(() {
configureDependencies(); // Call your real DI setup
getIt.pushNewScope(); // Shadow specific services with mocks
getIt.registerSingleton<ApiClient>(MockApiClient());
getIt.registerSingleton<Database>(MockDatabase());
});
tearDown(() async {
await getIt.popScope(); // Remove mocks, clean slate for next test
});Creando y Gestionando Scopes
Operaciones Básicas de Scope
// Push a new scope
getIt.pushNewScope(
scopeName: 'my-scope', // Optional: name for later reference
init: (getIt) {
// Register objects immediately
getIt.registerSingleton<Service>(ServiceImpl());
},
dispose: () {
// Cleanup when scope pops (called before object disposal)
print('Scope cleanup');
},
);
// Pop the current scope
await getIt.popScope();
// Pop multiple scopes to a named one
await getIt.popScopesTill('my-scope', inclusive: true);
// Drop a specific scope by name (without popping above it)
await getIt.dropScope('my-scope');
// Check if a scope exists
if (getIt.hasScope('session')) {
// ...
}
// Get current scope name
print(getIt
.currentScopeName); // Returns null for base scope, 'baseScope' for baseInicialización Asíncrona de Scope
Cuando la configuración del scope requiere operaciones asíncronas (cargar archivos de configuración, establecer conexiones):
const tenantId = 'tenant-123';
await getIt.pushNewScopeAsync(
scopeName: 'tenant-workspace',
init: (getIt) async {
// Load tenant configuration from file/database
final config = await loadTenantConfig(tenantId);
getIt.registerSingleton<TenantConfig>(config);
// Establish database connection
final dbConnection = DatabaseConnection();
await dbConnection.connect();
getIt.registerSingleton<DatabaseConnection>(dbConnection);
// Load cached data
final cache = CacheManager();
await cache.initialize(tenantId);
getIt.registerSingleton<CacheManager>(cache);
},
dispose: () async {
// Close connections
await getIt<DatabaseConnection>().close();
await getIt<CacheManager>().flush();
},
);Dependencias Asíncronas Entre Servicios
Para servicios con inicialización asíncrona que dependen entre sí, usa registerSingletonAsync con el parámetro dependsOn en su lugar. Mira la documentación de Objetos Asíncronos para detalles.
Características Avanzadas de Scope
Scopes Finales (Prevenir Registros Accidentales)
Previene condiciones de carrera bloqueando un scope después de la inicialización:
getIt.pushNewScope(
isFinal: true, // Can't register after init completes
init: (getIt) {
// MUST register everything here
getIt.registerSingleton<ServiceA>(ServiceA());
getIt.registerSingleton<ServiceB>(ServiceB());
},
);
// This throws an error - scope is final!
// getIt.registerSingleton<ServiceC>(ServiceC());Úsalo cuando:
- Construyas sistemas de plugins donde la configuración del scope debe ser atómica
- Prevenir registro accidental después de la inicialización del scope
Shadow Change Handlers
Los objetos pueden ser notificados cuando son sombreados o restaurados:
class StreamingService implements ShadowChangeHandlers {
StreamSubscription? _subscription;
final Stream<dynamic> dataStream = Stream.empty();
void init() {
_subscription = dataStream.listen(_handleData);
}
void _handleData(dynamic data) {
// Handle data
}
@override
void onGetShadowed(Object shadowingObject) {
// Another StreamingService is now active - pause our work
_subscription?.pause();
print('Paused: $shadowingObject is now handling streams');
}
@override
void onLeaveShadow(Object shadowingObject) {
// We're active again - resume work
_subscription?.resume();
print('Resumed: $shadowingObject was removed');
}
}Casos de uso:
- Servicios pesados en recursos que deberían pausarse cuando están inactivos
- Servicios con suscripciones que necesitan limpieza/restauración
- Prevenir trabajo duplicado en segundo plano
Notificaciones de Cambio de Scope
Recibe notificación cuando ocurre cualquier cambio de scope:
getIt.onScopeChanged = (bool pushed) {
if (pushed) {
print('New scope pushed - UI might need rebuild');
} else {
print('Scope popped - UI might need rebuild');
}
};Nota: watch_it maneja automáticamente las reconstrucciones de UI en cambios de scope vía rebuildOnScopeChanges.
Ciclo de Vida y Disposal de Scope
Orden de Disposal
Cuando se hace pop de un scope:
- La función dispose del scope es llamada (si se proporcionó)
- Las funciones dispose de objetos son llamadas en orden inverso de registro
- El scope es removido de la pila
void main() async {
// Get service from scope
final service = getIt<MyServiceImpl>();
service.doWork();
service.saveState();
// When exiting scope, call cleanup
await getIt.popScope();
// Scope is now disposed, service is unregistered
// Next access will get service from parent scope (if any)
final parentScopeService = getIt<MyServiceImpl>();
}Implementando la Interfaz Disposable
En lugar de pasar funciones dispose, implementa Disposable:
class MyService implements Disposable {
final Stream<dynamic> stream = Stream.empty();
StreamSubscription? _subscription;
void init() {
_subscription = stream.listen((data) {});
}
@override
Future<void> onDispose() async {
await _subscription?.cancel();
// Cleanup resources
}
}
void setup() {
// Automatically calls onDispose when scope pops or object is unregistered
getIt.registerSingleton<MyService>(MyService()..init());
}Reset vs Pop
// resetScope - clears all registrations in current scope but keeps scope
await getIt.resetScope(dispose: true);
// popScope - removes entire scope and restores previous
await getIt.popScope();Patrones Comunes
Flujo de Login/Logout
class AuthService {
Future<void> login(String username, String password) async {
final user = await getIt<ApiClient>().login(username, password);
// Push authenticated scope
getIt.pushNewScope(scopeName: 'authenticated');
getIt.registerSingleton<User>(user);
getIt.registerSingleton<ApiClient>(AuthenticatedApiClient(user.token));
getIt.registerSingleton<NotificationService>(NotificationService(user.id));
}
Future<void> logout() async {
// Pop scope - automatic cleanup of all authenticated services
await getIt.popScope();
// GuestUser (from base scope) is now active again
}
}Aplicaciones Multi-Tenant
class TenantManager {
Future<void> switchTenant(String tenantId) async {
// Pop previous tenant scope if exists
if (getIt.hasScope('tenant')) {
await getIt.popScope();
}
// Load new tenant
await getIt.pushNewScopeAsync(
scopeName: 'tenant',
init: (getIt) async {
final config = await loadTenantConfig(tenantId);
getIt.registerSingleton<TenantConfig>(config);
final database = await openTenantDatabase(tenantId);
getIt.registerSingleton<Database>(database);
final api = ApiClient(config.apiUrl);
getIt.registerSingleton<TenantServices>(
TenantServices(database, api),
);
},
);
}
Future<TenantConfig> loadTenantConfig(String tenantId) async {
await Future.delayed(const Duration(milliseconds: 10));
return TenantConfig('tenant_db', 'api_key_123');
}
}
Future<Database> openTenantDatabase(String tenantId) async {
await Future.delayed(const Duration(milliseconds: 10));
return Database();
}Feature Toggles con Scopes
class FeatureManager {
final Map<String, bool> _activeFeatures = {};
void enableFeature(String featureName, FeatureImplementation impl) {
if (_activeFeatures[featureName] == true) return;
getIt.pushNewScope(scopeName: 'feature-$featureName');
impl.register(getIt);
_activeFeatures[featureName] = true;
}
Future<void> disableFeature(String featureName) async {
if (_activeFeatures[featureName] != true) return;
await getIt.dropScope('feature-$featureName');
_activeFeatures[featureName] = false;
}
}Testing con Scopes
Usa scopes para sombrear servicios reales con mocks mientras mantienes el resto de tu configuración DI:
group('UserService Tests', () {
setUp(() {
// Call your app's real DI initialization
configureDependencies();
// Push scope to shadow specific services with test doubles
getIt.pushNewScope();
getIt.registerSingleton<ApiClient>(MockApiClient());
getIt.registerSingleton<Database>(MockDatabase());
// UserService uses real implementation but gets mock dependencies
});
tearDown(() async {
// Pop scope - removes mocks, restores real services
await getIt.popScope();
});
test('should load user data', () async {
// UserService gets MockApiClient and MockDatabase automatically
final service = getIt<UserService>();
print('service: $service');
final user = await service.loadUser('123');
expect(user.id, '123');
});
});Beneficios:
- No necesitas duplicar todos los registros en pruebas
- Solo simula lo necesario (ApiClient, Database)
- Otros servicios usan implementaciones reales
- Limpieza automática vía popScope()
Depurando Scopes
Verificar el Scope Actual
print('Current scope: ${getIt.currentScopeName}');
// Output: null (for unnamed scopes), 'session', 'baseScope', etc.Verificar Scope de Registro
final registration = getIt.findFirstObjectRegistration<MyService>();
print('registration: $registration');
print('Registered in scope: ${registration?.instanceName}');Verificar que Existe un Scope
if (getIt.hasScope('authenticated')) {
// Scope exists
} else {
// Not logged in
}Mejores Prácticas
✅ Haz
- Nombra tus scopes para depuración y gestión más fácil
- Usa el parámetro init para registrar objetos inmediatamente al empujar el scope
- Siempre haz await de popScope() para asegurar limpieza apropiada
- Implementa Disposable para limpieza automática en lugar de pasar funciones dispose
- Usa scopes para el ciclo de vida de lógica de negocio, no para estado de UI
❌️ No Hagas
- No uses scopes para estado temporal - usa parámetros o variables en su lugar
- No olvides hacer pop de scopes - fugas de memoria si los scopes se acumulan
- No dependas del orden de scope para lógica - usa dependencias explícitas
- No empujes scopes dentro de métodos build - usa
pushScopedewatch_itpara scopes ligados a widgets
Scopes Ligados a Widgets con watch_it
Para scopes ligados al tiempo de vida del widget, usa watch_it:
class UserProfilePage extends WatchingWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
Widget build(BuildContext context) {
// Automatically pushes scope when widget mounts
// Automatically pops scope when widget disposes
pushScope(init: (getIt) {
getIt.registerSingleton<ProfileController>(
ProfileController(userId: userId),
);
});
final controller = watchIt<ProfileController>();
return Scaffold(
body: Text(controller.userData.name),
);
}
}Mira la documentación de watch_it para detalles.
Referencia de API
Gestión de Scope
| Método | Descripción |
|---|---|
pushNewScope({init, scopeName, dispose, isFinal}) | Empuja un nuevo scope con registro inmediato opcional |
pushNewScopeAsync({init, scopeName, dispose}) | Empuja scope con inicialización asíncrona |
popScope() | Hace pop del scope actual y dispone objetos |
popScopesTill(name, {inclusive}) | Hace pop de todos los scopes hasta el scope nombrado |
dropScope(scopeName) | Elimina scope específico por nombre |
resetScope({dispose}) | Limpia los registros del scope actual |
hasScope(scopeName) | Verifica si existe un scope |
currentScopeName | Obtiene el nombre del scope actual (getter) |
Callbacks de Scope
| Propiedad | Descripción |
|---|---|
onScopeChanged | Llamado cuando se empuja/hace pop de un scope |
Ciclo de Vida del Objeto
| Interfaz | Descripción |
|---|---|
ShadowChangeHandlers | Implementa para ser notificado cuando es sombreado |
Disposable | Implementa para limpieza automática |
Ver También
- Registro de Objetos - Cómo registrar objetos
- Objetos Asíncronos - Trabajando con inicialización asíncrona
- Pruebas - Usando scopes en pruebas
watch_itpushScope - Scoping ligado a widgets