get-it-expert

Par flutter-it · get_it

Conseils d'expert sur le service locator get_it et l'injection de dépendances pour Flutter/Dart. Couvre l'enregistrement (singleton, factory, lazy, async), les scopes avec shadowing, l'initialisation async avec le pattern init(), la récupération, les tests avec mocking basé sur les scopes, et les patterns de production. À utiliser lors de travaux avec get_it, l'injection de dépendances, l'enregistrement de services, les scopes ou l'initialisation async.

npx skills add https://github.com/flutter-it/get_it --skill get-it-expert

get_it Expert - Service Locator & Dependency Injection

Quoi : Service locator type-safe avec recherche en O(1). Enregistrez les services globalement, accédez-y n'importe où sans BuildContext. Pur Dart, pas de code generation.

RÈGLES CRITIQUES

  • Enregistrez tous les services AVANT runApp()
  • pushNewScope() est synchrone. Utilisez pushNewScopeAsync() pour l'init asynchrone
  • popScope() EST asynchrone (retourne Future<void>)
  • allReady() retourne Future<void> - attendez-le ou utilisez FutureBuilder/watch_it
  • Les callbacks de dispose sont un paramètre des méthodes d'enregistrement, pas des méthodes séparées
  • Une fois que les singletons asynchrones sont initialisés (après allReady()), accédez-y avec getIt<T>() normal - pas besoin de getAsync
  • Si vous utilisez watch_it, un alias global di pour GetIt.I est déjà fourni - utilisez di<T>() au lieu de getIt<T>()

Enregistrement

final getIt = GetIt.instance;

void configureDependencies() {
  // Singleton - créé immédiatement
  getIt.registerSingleton<ApiClient>(ApiClient());

  // Singleton avec callback de dispose
  getIt.registerSingleton<StreamController>(
    StreamController(),
    dispose: (c) => c.close(),
  );

  // Lazy singleton - créé à premier accès
  getIt.registerLazySingleton<Database>(() => Database());

  // Factory - nouvelle instance à chaque appel
  getIt.registerFactory<Logger>(() => Logger());

  // Factory avec paramètres
  getIt.registerFactoryParam<Logger, String, void>(
    (tag, _) => Logger(tag),
  );

  // Instances nommées - utilisez lors de l'enregistrement de plusieurs instances du même type
  getIt.registerSingleton<Config>(devConfig, instanceName: 'dev');
  getIt.registerSingleton<Config>(prodConfig, instanceName: 'prod');
}

Initialisation Asynchrone

Motif préféré : Donnez aux services une méthode Future<T> init() qui retourne this. Cela garde la logique d'initialisation à l'intérieur de la classe et permet un enregistrement concis :

class DatabaseService {
  late final Database _db;

  Future<DatabaseService> init() async {
    _db = await Database.open('app.db');
    return this;  // Retournez toujours this
  }
}

void configureDependencies() {
  // Motif init() - initialisation concise et autonome
  getIt.registerSingletonAsync<DatabaseService>(
    () => DatabaseService().init(),
  );

  // Avec ordonnancement des dépendances
  getIt.registerSingletonAsync<ApiClient>(
    () => ApiClient().init(),
    dependsOn: [DatabaseService],
  );

  // Factory synchrone qui a besoin de dépendances asynchrones
  getIt.registerSingletonWithDependencies<AppModel>(
    () => AppModel(getIt<ApiClient>()),
    dependsOn: [ApiClient],
  );
}

Récupération

final api = getIt<ApiClient>();                        // get<T>() - lève une exception si absent
final api = getIt.maybeGet<ApiClient>();                // retourne null si absent
final api = await getIt.getAsync<ApiClient>();          // attend l'enregistrement asynchrone
final all = getIt.getAll<PaymentProcessor>();           // toutes les instances du type
final config = getIt<Config>(instanceName: 'dev');      // instance nommée
final logger = getIt<Logger>(param1: 'MyClass');        // factory avec paramètres

Scopes

// Pousser un scope (init synchrone)
getIt.pushNewScope(
  scopeName: 'user-session',
  init: (getIt) {
    getIt.registerSingleton<UserData>(currentUser);
    getIt.registerLazySingleton<UserPrefs>(() => UserPrefs(currentUser.id));
  },
);

// Pousser un scope (init asynchrone)
await getIt.pushNewScopeAsync(
  scopeName: 'user-session',
  init: (getIt) async {
    final prefs = await UserPrefs.load(currentUser.id);
    getIt.registerSingleton<UserPrefs>(prefs);
  },
);

// Dépiler un scope (toujours asynchrone - appelle les callbacks de dispose)
await getIt.popScope();

// Dépiler plusieurs scopes
await getIt.popScopesTill('base-scope', inclusive: false);

// Supprimer un scope spécifique par nom
await getIt.dropScope('user-session');

// Interroger les scopes
getIt.hasScope('user-session');    // bool
getIt.currentScopeName;            // String?

Shadowing de scope : Les scopes sont une pile de couches d'enregistrement. Quand vous enregistrez un type dans un nouveau scope qui existe déjà dans un scope inférieur, le nouvel enregistrement masque (cache) l'original. getIt<T>() recherche toujours de haut en bas, retournant la première correspondance. Dépiler un scope supprime ses enregistrements et restaure l'accès aux masqués en dessous. C'est ce qui rend les scopes utiles pour les tests (poussez un scope avec des mocks, dépliez-le dans tearDown), pour les sessions utilisateur (poussez des services spécifiques à l'utilisateur qui masquent les defaults), et pour regrouper les objets liés qui doivent être supprimés ensemble selon la logique métier (par ex., poussez un scope pour un panier d'achat - le dépiler supprime tous les services liés au panier à la fois).

État Prêt

// Attendre TOUS les enregistrements asynchrones
await getIt.allReady(timeout: Duration(seconds: 10));

// Attendre un type spécifique
await getIt.isReady<Database>(timeout: Duration(seconds: 5));

// Vérifications synchrones (pas d'attente)
getIt.allReadySync();              // bool
getIt.isReadySync<Database>();     // bool

Intégration UI : Utilisez FutureBuilder avec getIt.allReady() pour afficher un écran de splash pendant l'initialisation des services asynchrones. Si vous utilisez watch_it, préférez sa fonction allReady() à l'intérieur d'un WatchingWidget (voir skill watch-it-expert).

Comptage des Références

Pour les scénarios comme la navigation récursive (même page poussée plusieurs fois) :

// Enregistre seulement si pas déjà enregistré, incrémente le compteur de ref
getIt.registerSingletonIfAbsent<PageData>(() => PageData(id));

// Décrémente le compteur de ref, supprime seulement quand le compteur atteint 0
getIt.releaseInstance<PageData>(ignoreReferenceCount: false);

Méthodes Utilitaires

getIt.isRegistered<ApiClient>();                       // bool
getIt.unregister<ApiClient>();                         // supprimer l'enregistrement
getIt.resetLazySingleton<Database>();                  // recréer à prochain accès
getIt.resetLazySingletons(inAllScopes: true);          // réinitialisation en masse
getIt.checkLazySingletonInstanceExists<Database>();    // est-il instancié ?
getIt.reset();                                         // tout effacer (pour les tests)
getIt.allowReassignment = true;                        // autoriser la réécriture des enregistrements
getIt.enableRegisteringMultipleInstancesOfOneType();   // autoriser les multiples non nommés

Anti-Motifs

// ❌ Accès à un service asynchrone avant allReady()
configureDependencies();
final db = getIt<Database>();  // LÈVE UNE EXCEPTION - pas prêt encore

// ✅ Attendez d'abord
await getIt.allReady();
final db = getIt<Database>();  // Sûr

// ❌ await sur pushNewScope (c'est void, pas Future)
await getIt.pushNewScope(scopeName: 'x');  // Ne compilera pas

// ✅ Utilisez pushNewScopeAsync pour l'init asynchrone
await getIt.pushNewScopeAsync(
  scopeName: 'x',
  init: (getIt) async { ... },
);
// OU utilisez pushNewScope synchrone sans await
getIt.pushNewScope(scopeName: 'x', init: (getIt) { ... });

Tests

// Option 1 : Basée sur scopes (préféré) - les mocks masquent les enregistrements réels
setUp(() {
  GetIt.I.pushNewScope(
    init: (getIt) {
      getIt.registerSingleton<ApiClient>(MockApiClient());
    },
  );
});
tearDown(() async {
  await GetIt.I.popScope();
});

// Option 2 : Injection hybride par constructeur (commodité optionnelle)
class MyService {
  final ApiClient api;
  MyService({ApiClient? api}) : api = api ?? getIt<ApiClient>();
}
// Test : MyService(api: MockApiClient())

Motifs de Production

DI en deux phases (base + scope jetable) :

void setupBaseServices() {
  di.registerSingleton<ApiClient>(createApiClient());
  di.registerSingleton<CacheManager>(WcImageCacheManager());
}

Future<void> setupThrowableScope() async {
  di.pushNewScope(scopeName: 'throwableScope');
  di.registerLazySingletonAsync<StoryManager>(
    () async => StoryManager().init(),
    dispose: (m) => m.dispose(),
    dependsOn: [UserManager],
  );
}

// En récupération d'erreur : réinitialiser le scope jetable
await di.popScopesTill('throwableScope', inclusive: true);
await setupThrowableScope();

Déconnexion / nettoyage de scope — utilisez popScopesTill pour dépiler plusieurs scopes à la fois au lieu de vérifier et dépiler manuellement chacun :

// ❌ Dépilage manuel scope par scope
void onLogout() {
  if (di.hasScope('chat')) di.popScope();
  if (di.hasScope('auth')) di.popScope();
}

// ✅ Utilisez popScopesTill pour dépiler tout ce qui est au-dessus (et incluant) le scope auth
Future<void> onLogout() async {
  if (di.hasScope('auth')) {
    await di.popScopesTill('auth', inclusive: true);
  }
}

Skills similaires