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. UtilisezpushNewScopeAsync()pour l'init asynchronepopScope()EST asynchrone (retourneFuture<void>)allReady()retourneFuture<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 avecgetIt<T>()normal - pas besoin degetAsync - Si vous utilisez watch_it, un alias global
dipourGetIt.Iest déjà fourni - utilisezdi<T>()au lieu degetIt<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);
}
}