Skip to content

Avanzado

Implementando la Interfaz Disposable

En lugar de pasar una función de disposal en el registro o al empujar un Scope, desde V7.0 el método onDispose() de tu objeto será llamado si el objeto que registras implementa la interfaz Disposable:

dart
abstract class Disposable {
  FutureOr onDispose();
}

Encontrar Todas las Instancias por Tipo: findAll<T>()

Encuentra todas las instancias registradas que coincidan con un tipo dado con opciones poderosas de filtrado y coincidencia.

dart
// ignore_for_file: missing_function_body, unused_element
List<T> findAll<T>({
  bool includeSubtypes = true,
  bool inAllScopes = false,
  String? onlyInScope,
  bool includeMatchedByRegistrationType = true,
  bool includeMatchedByInstance = true,
  bool instantiateLazySingletons = false,
  bool callFactories = false,
}) =>
    [];

Nota de Rendimiento

A diferencia de las búsquedas O(1) basadas en Map de get_it, findAll() realiza una búsqueda lineal O(n) a través de todos los registros. Usa con moderación en código crítico de rendimiento. El rendimiento puede mejorarse limitando la búsqueda a un solo scope usando onlyInScope.

Parámetros:

Coincidencia de Tipo:

  • includeSubtypes - Si es true (por defecto), coincide con T y todos los subtipos; si es false, coincide solo con el tipo exacto T

Control de Scope:

  • inAllScopes - Si es true, busca en todos los scopes (por defecto: false, solo scope actual)
  • onlyInScope - Busca solo en el scope nombrado (tiene precedencia sobre inAllScopes)

Estrategia de Coincidencia:

  • includeMatchedByRegistrationType - Coincidir por tipo registrado (por defecto: true)
  • includeMatchedByInstance - Coincidir por tipo de instancia real (por defecto: true)

Efectos Secundarios:

  • instantiateLazySingletons - Instanciar lazy singletons que coincidan (por defecto: false)
  • callFactories - Llamar factories que coincidan para incluir sus instancias (por defecto: false)

Ejemplo - Coincidencia básica de tipos:

dart
abstract class IOutput {
  void write(String message);
}

class FileOutput implements IOutput {
  @override
  void write(String message) => File('log.txt').writeAsStringSync(message);
}

class ConsoleOutput implements IOutput {
  @override
  void write(String message) => print(message);
}

// Register different implementation types

void main() {
  getIt.registerSingleton<FileOutput>(FileOutput());
  getIt.registerLazySingleton<ConsoleOutput>(() => ConsoleOutput());

  // Find by interface (registration type matching)
  final outputs = getIt.findAll<IOutput>();
  print('outputs: $outputs');
  // Returns: [FileOutput] only (ConsoleOutput not instantiated yet)
}
Ejemplo - Incluir lazy singletons
dart
void main() async {
// Instantiate lazy singletons that match
  final all = getIt.findAll<IOutput>(
    instantiateLazySingletons: true,
  );
// Returns: [FileOutput, ConsoleOutput]
// ConsoleOutput is now created and cached
  print('Found: $all');
}
Ejemplo - Incluir factories
dart
void main() async {
  getIt.registerFactory<IOutput>(() => RemoteOutput('https://api.example.com'));

// Include factories by calling them
  final withFactories = getIt.findAll<IOutput>(
    instantiateLazySingletons: true,
    callFactories: true,
  );
// Returns: [FileOutput, ConsoleOutput, RemoteOutput]
// Each factory call creates a new instance
}
Ejemplo - Coincidencia exacta de tipo
dart
class BaseLogger {}

class FileLogger extends BaseLogger {}

class ConsoleLogger extends BaseLogger {}

void main() {
  getIt.registerSingleton<BaseLogger>(FileLogger());
  getIt.registerSingleton<BaseLogger>(ConsoleLogger());

  // Find subtypes (default)
  final allLoggers = getIt.findAll<BaseLogger>();
  print('allLoggers: $allLoggers');
  // Returns: [FileLogger, ConsoleLogger]

  // Find exact type only
  final exactBase = getIt.findAll<BaseLogger>(
    includeSubtypes: false,
  );
  // Returns: [] (no exact BaseLogger instances, only subtypes)
}
Ejemplo - Tipo de Instancia vs Tipo de Registro
dart
void main() async {
// Register as FileOutput but it implements IOutput
  getIt.registerSingleton<FileOutput>(FileOutput('/path/to/file.txt'));

// Match by registration type
  final byRegistration = getIt.findAll<IOutput>(
    includeMatchedByRegistrationType: true,
    includeMatchedByInstance: false,
  );
// Returns: [] (registered as FileOutput, not IOutput)

// Match by instance type
  final byInstance = getIt.findAll<IOutput>(
    includeMatchedByRegistrationType: false,
    includeMatchedByInstance: true,
  );
// Returns: [FileOutput] (instance implements IOutput)
}
Ejemplo - Control de scope
dart
void main() {
  // Base scope
  getIt.registerSingleton<IOutput>(FileOutput('/tmp/output.txt'));

  // Push scope
  getIt.pushNewScope(scopeName: 'session');
  getIt.registerSingleton<IOutput>(ConsoleOutput());

  // Current scope only (default)
  final current = getIt.findAll<IOutput>();
  print('current: $current');
  // Returns: [ConsoleOutput]

  // All scopes
  final all = getIt.findAll<IOutput>(inAllScopes: true);
  print('all: $all');
  // Returns: [ConsoleOutput, FileOutput]

  // Specific scope
  final base = getIt.findAll<IOutput>(onlyInScope: 'baseScope');
  print('base: $base');
  // Returns: [FileOutput]
}

Casos de uso:

  • Encontrar todas las implementaciones de una interfaz de plugin
  • Recopilar todos los validators/processors registrados
  • Visualización de grafo de dependencias en tiempo de ejecución
  • Pruebas: verificar que todos los tipos esperados están registrados
  • Herramientas de migración: encontrar instancias de tipos deprecated

Reglas de validación:

  • includeSubtypes=false requiere includeMatchedByInstance=false
  • instantiateLazySingletons=true requiere includeMatchedByRegistrationType=true
  • callFactories=true requiere includeMatchedByRegistrationType=true

Lanza:

  • StateError si onlyInScope no existe
  • ArgumentError si se violan las reglas de validación

Conteo de Referencias

El conteo de referencias ayuda a gestionar el ciclo de vida de singletons cuando múltiples consumidores podrían necesitar la misma instancia, especialmente útil para escenarios recursivos como navegación.

El Problema

Imagina una página de detalle que puede ser empujada recursivamente (ej., ver items relacionados, navegar a través de una jerarquía):

Home → DetailPage(item1) → DetailPage(item2) → DetailPage(item3)

Sin conteo de referencias:

  • Primera DetailPage registra DetailService
  • Segunda DetailPage intenta registrar → Error o debe saltarse el registro
  • Primera DetailPage hace pop, dispone el servicio → Rompe las páginas restantes

La Solución: registerSingletonIfAbsent y releaseInstance

dart
T registerSingletonIfAbsent<T>(
  T Function() factoryFunc, {
  String? instanceName,
  DisposingFunc<T>? dispose,
}) =>
    factoryFunc();

void releaseInstance(Object instance) {}

Cómo funciona:

  1. Primera llamada: Crea instancia, registra, establece contador de referencia a 1
  2. Llamadas subsecuentes: Devuelve instancia existente, incrementa contador
  3. releaseInstance: Decrementa contador
  4. Cuando el contador llega a 0: Desregistra y dispone

Ejemplo de Navegación Recursiva

dart
class DetailService extends ChangeNotifier {
  final String itemId;
  String? data;
  bool isLoading = false;

  DetailService(this.itemId) {
    // Trigger async loading in constructor (fire and forget)
    _loadData();
  }

  Future<void> _loadData() async {
    if (data != null) return; // Already loaded

    isLoading = true;
    notifyListeners();

    print('Loading data for $itemId from backend...');
    // Simulate backend call
    await Future.delayed(Duration(seconds: 1));
    data = 'Data for $itemId';

    isLoading = false;
    notifyListeners();
  }
}

class DetailPage extends WatchingWidget {
  final String itemId;
  const DetailPage(this.itemId);

  @override
  Widget build(BuildContext context) {
    // Register once when widget is created, dispose when widget is disposed
    callOnce(
      (context) {
        // Register or get existing - increments reference count
        getIt.registerSingletonIfAbsent<DetailService>(
          () => DetailService(itemId),
          instanceName: itemId,
        );
      },
      dispose: () {
        // Decrements reference count when widget disposes
        getIt.releaseInstance(getIt<DetailService>(instanceName: itemId));
      },
    );

    // Watch the service - rebuilds when notifyListeners() called
    final service = watchIt<DetailService>(instanceName: itemId);

    return Scaffold(
      appBar: AppBar(title: Text('Detail $itemId')),
      body: service.isLoading
          ? const Center(child: CircularProgressIndicator())
          : Column(
              children: [
                Text(service.data ?? 'No data'),
                ElevatedButton(
                  onPressed: () {
                    // Can push same page recursively
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (_) => DetailPage('related-$itemId'),
                      ),
                    );
                  },
                  child: const Text('View Related'),
                ),
              ],
            ),
    );
  }
}

Flujo:

Push DetailPage(item1)      → Crear servicio, cargar datos, refCount = 1
  Push DetailPage(item2)    → Crear servicio, cargar datos, refCount = 1
    Push DetailPage(item1)  → Obtener existente (¡NO recarga!), refCount = 2
    Pop DetailPage(item1)   → Release, refCount = 1 (servicio permanece)
  Pop DetailPage(item2)     → Release, refCount = 0 (servicio dispuesto)
Pop DetailPage(item1)       → Release, refCount = 0 (servicio dispuesto)

Beneficios:

  • ✅ Servicio creado síncronamente (no se necesita factory async)
  • ✅ Carga async disparada en constructor
  • ✅ Sin carga duplicada para el mismo item (verificado antes de cargar)
  • ✅ Gestión automática de memoria vía conteo de referencias
  • ✅ Actualizaciones reactivas de UI vía `watch_it` (reconstruye en cambios de estado)
  • ✅ ChangeNotifier automáticamente dispuesto cuando refCount llega a 0
  • ✅ Cada itemId únicamente identificado vía `instanceName`

Integración Clave: Este ejemplo demuestra cómo get_it (conteo de referencias) y watch_it (UI reactiva) funcionan juntos sin problemas para patrones de navegación complejos.


Liberación Forzada: ignoreReferenceCount

En casos raros, podrías necesitar forzar el desregistro sin importar el conteo de referencias:

dart
// Force unregister even if refCount > 0
getIt.unregister<MyService>(ignoreReferenceCount: true);

Usa con Precaución

Solo usa ignoreReferenceCount: true cuando estés seguro de que ningún otro código está usando la instancia. Esto puede causar crashes si otras partes de tu app aún mantienen referencias.

Cuándo Usar Conteo de Referencias

Buenos casos de uso:

  • Navegación recursiva (misma página empujada múltiples veces)
  • Servicios necesarios por múltiples características activas simultáneamente
  • Estructuras de componentes jerárquicas complejas

❌️ No uses cuando:

  • Singleton simple que vive durante el tiempo de vida de la app (usa registerSingleton regular)
  • Relación uno-a-uno widget-servicio (usa scopes)
  • Pruebas (usa scopes para sombrear en su lugar)

Mejores Prácticas

  1. Siempre empareja registro con liberación: Cada registerSingletonIfAbsent debe tener un releaseInstance correspondiente
  2. Almacena referencia de instancia: Mantén la instancia devuelta para que puedas liberar la correcta
  3. Libera en dispose/cleanup: Ata la liberación al ciclo de vida del widget/componente
  4. Documenta recursos compartidos: Deja claro cuándo un servicio usa conteo de referencias

Métodos Utilitarios

Recuperación Segura: maybeGet<T>()

Devuelve null en lugar de lanzar una excepción si el tipo no está registrado. Útil para dependencias opcionales y feature flags.

dart
/// like [get] but returns null if the instance is not found
T? maybeGet<T extends Object>({
  dynamic param1,
  dynamic param2,
  String? instanceName,
  Type? type,
});

Ejemplo:

dart
// Feature flag scenario
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final logger = getIt.maybeGet<Logger>();
    print('logger: $logger');
    logger?.log('Building MyWidget'); // Safe even if Logger not registered

    return const Text('Hello');
  }
}

// Graceful degradation
Widget getUIForPremiumStatus() {
  final premiumFeature = getIt.maybeGet<PremiumFeature>();
  print('premiumFeature: $premiumFeature');
  if (premiumFeature != null) {
    return PremiumUI(feature: premiumFeature);
  } else {
    return const BasicUI(); // Fallback when premium not available
  }
}

void main() {
  final analyticsService = getIt.maybeGet<AnalyticsService>();
  print('analyticsService: $analyticsService');
  if (analyticsService != null) {
    analyticsService.trackEvent('user_action');
  }

  final ui = getUIForPremiumStatus();
  print('UI: $ui');
}

Cuándo usar:

  • ✅ Características opcionales que pueden o no estar registradas
  • ✅ Feature flags (servicio registrado solo cuando la característica está habilitada)
  • ✅ Servicios específicos de plataforma (podrían no existir en todas las plataformas)
  • ✅ Escenarios de degradación elegante

No uses cuando:

  • ❌️ La dependencia es requerida - usa get<T>() para fallar rápido
  • ❌️ El registro faltante indica un bug - la excepción es útil

Renombrado de Instancia: changeTypeInstanceName()

Renombra una instancia registrada sin desregistrar y re-registrar (evita disparar funciones de disposal).

dart
// ignore_for_file: missing_function_body, unused_element
void changeTypeInstanceName<T>({
  String? instanceName,
  required String newInstanceName,
  T? instance,
}) {}

Ejemplo:

dart
class UserModel extends ChangeNotifier {
  String username;
  String email;

  UserModel(this.username, this.email);

  Future<void> updateUsername(String newUsername) async {
    // Update on backend via API (stubbed)
    final oldUsername = username;
    username = newUsername;

    // Rename the instance in GetIt to match new username
    getIt.changeTypeInstanceName<UserModel>(
      instanceName: oldUsername,
      newInstanceName: newUsername,
    );

    notifyListeners();
  }
}

Casos de uso:

  • Actualizaciones de perfil de usuario donde el nombre de usuario es el identificador de instancia
  • Nombres de entidades dinámicas que pueden cambiar en tiempo de ejecución
  • Evitar efectos secundarios de disposal del ciclo unregister/register
  • Mantener estado de instancia mientras se actualiza su identificador

Evita Dispose

A diferencia de unregister() + register(), esto no dispara funciones de disposal, preservando el estado de la instancia.


Introspección de Lazy Singleton: checkLazySingletonInstanceExists()

Verifica si un lazy singleton ha sido instanciado aún (sin disparar su creación).

dart
// ignore_for_file: missing_function_body, unused_element
import 'package:get_it/get_it.dart';

bool checkLazySingletonInstanceExists<T>({
  String? instanceName,
}) =>
    false;

Ejemplo:

dart
// Register lazy singleton
getIt.registerLazySingleton<HeavyService>(() => HeavyService());

// Check if it's been created yet
if (getIt.checkLazySingletonInstanceExists<HeavyService>()) {
  print('HeavyService already created');
} else {
  print('HeavyService not created yet - will be lazy loaded');
}

// Access triggers creation
final service = getIt<HeavyService>();
print('service: $service');

// Now it exists
assert(getIt.checkLazySingletonInstanceExists<HeavyService>() == true);

Casos de uso:

  • Monitoreo de rendimiento (rastrear qué servicios han sido inicializados)
  • Inicialización condicional (pre-calentar servicios si no se crearon)
  • Probar comportamiento de carga lazy
  • Depurar problemas de orden de inicialización

Ejemplo - Pre-calentamiento:

dart
void preWarmCriticalServices() {
  // Only initialize if not already created
  if (!getIt.checkLazySingletonInstanceExists<DatabaseService>()) {
    getIt<DatabaseService>(); // Trigger creation
  }

  if (!getIt.checkLazySingletonInstanceExists<CacheService>()) {
    getIt<CacheService>(); // Trigger creation
  }
}

Resetear Todos los Lazy Singletons: resetLazySingletons()

Resetea todos los lazy singletons instanciados de una vez. Esto limpia sus instancias para que sean recreadas en el siguiente acceso.

dart
Future<void> resetLazySingletons({
  bool dispose = true,
  bool inAllScopes = false,
  String? onlyInScope,
}) async {}

Parámetros:

  • dispose - Si es true (por defecto), llama funciones de disposal antes de resetear
  • inAllScopes - Si es true, resetea lazy singletons a través de todos los scopes
  • onlyInScope - Resetea solo en el scope nombrado (tiene precedencia sobre inAllScopes)

Ejemplo - Uso básico:

dart
void main() async {
  // Register lazy singletons
  getIt.registerLazySingleton<CacheService>(() => CacheService());
  getIt.registerLazySingleton<UserPreferences>(() => UserPreferences());

  // Access them (creates instances)
  final cache = getIt<CacheService>();
  print('cache: $cache');
  final prefs = getIt<UserPreferences>();
  print('prefs: $prefs');

  // Reset all lazy singletons in current scope
  await getIt.resetLazySingletons();

  // Next access creates fresh instances
  final newCache = getIt<CacheService>();
  print('newCache: $newCache'); // New instance
}

Ejemplo - Con scopes:

dart
void main() async {
  // Base scope lazy singletons
  getIt.registerLazySingleton<GlobalCache>(() => GlobalCache());

  // Push scope and register more
  getIt.pushNewScope(scopeName: 'session');
  getIt.registerLazySingleton<SessionCache>(() => SessionCache());
  getIt.registerLazySingleton<UserState>(() => UserState());

  // Access them
  final globalCache = getIt<GlobalCache>();
  print('globalCache: $globalCache');
  final sessionCache = getIt<SessionCache>();
  print('sessionCache: $sessionCache');

  // Reset only current scope ('session')
  await getIt.resetLazySingletons();
  // GlobalCache NOT reset, SessionCache and UserState ARE reset

  // Reset all scopes
  await getIt.resetLazySingletons(inAllScopes: true);
  // Both GlobalCache and SessionCache are reset

  // Reset only specific scope
  await getIt.resetLazySingletons(onlyInScope: 'baseScope');
  // Only GlobalCache is reset
}

Casos de uso:

  • Reset de estado entre pruebas
  • Logout de usuario (limpiar lazy singletons específicos de sesión)
  • Optimización de memoria (resetear cachés que pueden ser recreados)
  • Limpieza específica de scope sin hacer pop del scope

Comportamiento:

  • Solo resetea lazy singletons que han sido instanciados
  • Los lazy singletons no instanciados no se afectan
  • Los singletons regulares y factories no se afectan
  • Soporta funciones de disposal tanto sync como async

Introspección Avanzada: findFirstObjectRegistration<T>()

Obtiene metadatos sobre un registro sin recuperar la instancia.

dart
/// find the first registration that matches the type [T]/[instanceName] or the [instance]
ObjectRegistration? findFirstObjectRegistration<T extends Object>({
  Object? instance,
  String? instanceName,
});

Ejemplo:

dart
final registration = getIt.findFirstObjectRegistration<MyService>();
print('registration: $registration');

if (registration != null) {
  print(
      'Type: ${registration.registrationType}'); // factory, singleton, lazy, etc.
  print('Instance name: ${registration.instanceName}');
  print('Is async: ${registration.isAsync}');
  print('Is ready: ${registration.isReady}');
}

Casos de uso:

  • Construir herramientas/utilidades de depuración sobre GetIt
  • Visualización de grafo de dependencias en tiempo de ejecución
  • Gestión avanzada del ciclo de vida
  • Depuración de problemas de registro

Accediendo a un objeto dentro de GetIt por un tipo de tiempo de ejecución

En ocasiones raras podrías enfrentarte con el problema de que no conoces el tipo que quieres recuperar de GetIt en tiempo de compilación, lo que significa que no puedes pasarlo como parámetro genérico. Para esto las funciones get tienen un parámetro type opcional

dart
getIt.registerSingleton(TestClass());

final instance1 = getIt.get(type: TestClass);
print('instance1: $instance1');

expect(instance1 is TestClass, true);

Ten cuidado de que la variable receptora tenga el tipo correcto y no pases type y un parámetro genérico.

Más de una instancia de GetIt

Aunque no se recomienda, puedes crear tu propia instancia independiente de GetIt si no quieres compartir tu locator con algún otro paquete o porque la física de tu planeta lo demanda 😃

dart
/// To make sure you really know what you are doing
/// you have to first enable this feature:
GetIt myOwnInstance = GetIt.asNewInstance();

Esta nueva instancia no comparte ningún registro con la instancia singleton.

Publicado bajo la Licencia MIT.