Skip to content

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.

Visualización de la Pila de Scopes

Visualización de la Pila de Scopes

Cómo Funciona el Shadowing

dart
// 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

dart
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

dart
// 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

dart
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

dart
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

dart
// 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 base

Inicialización Asíncrona de Scope

Cuando la configuración del scope requiere operaciones asíncronas (cargar archivos de configuración, establecer conexiones):

dart
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:

dart
  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:

dart
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:

dart
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:

  1. La función dispose del scope es llamada (si se proporcionó)
  2. Las funciones dispose de objetos son llamadas en orden inverso de registro
  3. El scope es removido de la pila
dart
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:

dart
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

dart
// 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

dart
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
dart
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
dart
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:

dart
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

dart
  print('Current scope: ${getIt.currentScopeName}');
// Output: null (for unnamed scopes), 'session', 'baseScope', etc.
Verificar Scope de Registro
dart
final registration = getIt.findFirstObjectRegistration<MyService>();
print('registration: $registration');
print('Registered in scope: ${registration?.instanceName}');
Verificar que Existe un Scope
dart
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 pushScope de watch_it para scopes ligados a widgets

Scopes Ligados a Widgets con watch_it

Para scopes ligados al tiempo de vida del widget, usa watch_it:

dart
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étodoDescripció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
currentScopeNameObtiene el nombre del scope actual (getter)

Callbacks de Scope

PropiedadDescripción
onScopeChangedLlamado cuando se empuja/hace pop de un scope

Ciclo de Vida del Objeto

InterfazDescripción
ShadowChangeHandlersImplementa para ser notificado cuando es sombreado
DisposableImplementa para limpieza automática

Ver También

Publicado bajo la Licencia MIT.