Async Objects
Overview
GetIt provides comprehensive support for asynchronous object creation and initialization. This is essential for objects that need to perform async operations during creation (database connections, network calls, file I/O) or that depend on other async objects being ready first.
Key capabilities:
- ✅ Async Factories - Create new instances asynchronously on each access
- ✅ Async Singletons - Create singletons with async initialization
- ✅ Dependency Management - Automatically wait for dependencies before initialization
- ✅ Startup Orchestration - Coordinate complex initialization sequences
- ✅ Manual Signaling - Fine-grained control over ready state
Quick Reference
Async Registration Methods
| Method | When Created | How Many Instances | Lifetime | Best For |
|---|---|---|---|---|
| registerFactoryAsync | Every getAsync() | Many | Per request | Async operations on each access |
| registerCachedFactoryAsync | First access + after GC | Reused while in memory | Until garbage collected | Performance optimization for expensive async operations |
| registerSingletonAsync | Immediately at registration | One | Permanent | App-level services with async setup |
| registerLazySingletonAsync | First getAsync() | One | Permanent | Expensive async services not always needed |
| registerSingletonWithDependencies | After dependencies ready | One | Permanent | Services depending on other services |
Async Factories
Async factories create a new instance on each call to getAsync() by executing an asynchronous factory function.
registerFactoryAsync
Creates a new instance every time you call getAsync<T>().
void registerFactoryAsync<T extends Object>(
FactoryFuncAsync<T> factoryFunc, {
String? instanceName,
});Parameters:
factoryFunc- Async function that creates and returns the instanceinstanceName- Optional name to register multiple factories of the same type
Example:
void main() async {
// Register async factory
getIt.registerFactoryAsync<DatabaseConnection>(
() async {
final conn = DatabaseConnection();
await conn.connect();
return conn;
},
);
// Usage - creates new instance each time
final db1 = await getIt.getAsync<DatabaseConnection>();
final db2 = await getIt.getAsync<DatabaseConnection>(); // Different instance
print('db1 == db2: ${identical(db1, db2)}'); // false - different instances
}registerCachedFactoryAsync
Like registerFactoryAsync, but caches the instance with a weak reference. Returns the cached instance if it's still in memory; otherwise creates a new one.
void registerCachedFactoryAsync<T extends Object>(
FactoryFuncAsync<T> factoryFunc, {
String? instanceName,
});Example:
void main() async {
// Cached async factory
getIt.registerCachedFactoryAsync<HeavyResource>(
() async {
final resource = HeavyResource();
await resource.initialize();
return resource;
},
);
// First access - creates new instance
final resource1 = await getIt.getAsync<HeavyResource>();
// While still in memory - returns cached instance
final resource2 = await getIt.getAsync<HeavyResource>();
print(
'resource1 == resource2: ${identical(resource1, resource2)}'); // true - same instance
}Async Factories with Parameters
Like regular factories, async factories can accept up to two parameters.
void registerFactoryParamAsync<T, P1, P2>(
FactoryFuncParamAsync<T, P1, P2> factoryFunc, {
String? instanceName,
});
void registerCachedFactoryParamAsync<T, P1, P2>(
FactoryFuncParamAsync<T, P1, P2> factoryFunc, {
String? instanceName,
});Example:
// Register async factory with two parameters
getIt.registerFactoryParamAsync<UserViewModel, String, int>(
(userId, age) async {
// Simulate async initialization (e.g., fetch from API)
await Future.delayed(Duration(milliseconds: 100));
return UserViewModel(userId, age: age);
},
);
// Access with parameters
final vm = await getIt.getAsync<UserViewModel>(
param1: 'user-123',
param2: 25,
);Async Singletons
Async singletons are created once with async initialization and live for the lifetime of the registration (until unregistered or scope is popped).
registerSingletonAsync
Registers a singleton with an async factory function that's executed immediately. The singleton is marked as ready when the factory function completes (unless signalsReady is true).
void registerSingletonAsync<T extends Object>(
FactoryFuncAsync<T> factoryFunc, {
String? instanceName,
Iterable<Type>? dependsOn,
bool? signalsReady,
DisposingFunc<T>? dispose,
void Function(T instance)? onCreated,
});Parameters:
factoryFunc- Async function that creates the singleton instanceinstanceName- Optional name to register multiple singletons of the same typedependsOn- List of types this singleton depends on (waits for them to be ready first)signalsReady- If true, you must manually callsignalReady()to mark as readydispose- Optional cleanup function called when unregistering or resettingonCreated- Optional callback invoked after the instance is created
Example:
class RestService {
Future<RestService> init() async {
// do your async initialisation...
await Future.delayed(Duration(seconds: 2));
return this;
}
}
Future<void> setup() async {
// Pattern: Create instance and call init() in one expression
getIt.registerSingletonAsync<RestService>(() async => RestService().init());
// Wait for all async singletons to be ready
await getIt.allReady();
// Now access normally
final service = getIt<RestService>();
}Common Mistake: Using signalsReady with registerSingletonAsync
Most of the time, you DON'T need signalsReady: true with registerSingletonAsync.
The async factory completion automatically signals ready when it returns. Only use signalsReady: true if you need multi-stage initialization where the factory completes but you have additional async work before the instance is truly ready.
Common error pattern:
class MyService {
Future<void> initialize() async {
await Future.delayed(Duration(milliseconds: 100));
}
}
void configureIncorrect() {
// ❌ This will throw: "This instance is not available in GetIt"
getIt.registerSingletonAsync<MyService>(
() async {
final service = MyService();
await service.initialize();
// ❌ ERROR: Instance not in GetIt yet!
// The factory hasn't returned, so GetIt doesn't know about this instance
getIt.signalReady(service);
return service;
},
signalsReady: true,
);
}Why it fails: You can't call signalReady(instance) from inside the factory because the instance isn't registered yet.
Correct alternatives:
Option 1 - Let async factory auto-signal (recommended):
void configure() {
// ✅ Correct - no signalsReady needed
getIt.registerSingletonAsync<MyService>(
() async {
final service = MyService();
await service.initialize();
return service; // Automatically signals ready
},
);
}Option 2 - Use registerSingleton for post-registration signaling:
class MyService {
MyService() {
_initialize();
}
Future<void> _initialize() async {
await loadData();
GetIt.instance.signalReady(this); // ✅ Now it's in GetIt
}
Future<void> loadData() async {
await Future.delayed(Duration(milliseconds: 100));
}
}
void configure() {
// ✅ Correct - instance registered immediately
getIt.registerSingleton<MyService>(
MyService(),
signalsReady: true, // Must manually signal
);
}Option 3 - Implement WillSignalReady interface:
class MyService implements WillSignalReady {
MyService() {
_initialize();
}
Future<void> _initialize() async {
await loadData();
GetIt.instance.signalReady(this);
}
Future<void> loadData() async {
await Future.delayed(Duration(milliseconds: 100));
}
}
void configure() {
// ✅ Correct - interface-based signaling
getIt.registerSingleton<MyService>(MyService());
// No signalsReady parameter needed - interface detected
}See Manual Ready Signaling for more details.
registerLazySingletonAsync
Registers a singleton with an async factory function that's executed on first access (when you call getAsync<T>() for the first time).
void registerLazySingletonAsync<T extends Object>(
FactoryFuncAsync<T> factoryFunc, {
String? instanceName,
DisposingFunc<T>? dispose,
void Function(T instance)? onCreated,
bool useWeakReference = false,
});Parameters:
factoryFunc- Async function that creates the singleton instanceinstanceName- Optional name to register multiple singletons of the same typedispose- Optional cleanup function called when unregistering or resettingonCreated- Optional callback invoked after the instance is createduseWeakReference- If true, uses weak reference (allows garbage collection if not used)
Example:
void configureDependencies() {
// Lazy async singleton - created on first access
getIt.registerLazySingletonAsync<CacheService>(
() async {
final cache = CacheService();
await cache.loadFromDisk();
return cache;
},
);
// With weak reference - allows GC when not in use
getIt.registerLazySingletonAsync<ImageCache>(
() async => ImageCache.load(),
useWeakReference: true,
);
}
Future<void> main() async {
configureDependencies();
// First access - triggers creation
final cache = await getIt.getAsync<CacheService>();
// Subsequent access - returns existing instance
final cache2 = await getIt.getAsync<CacheService>(); // Same instance
}Lazy Async Singletons and allReady()
registerLazySingletonAsync does not block allReady() because the factory function is not called until first access. However, once accessed, you can use isReady() to wait for its completion.
Accessing Async Objects
Async Objects Become Normal After Initialization
Once an async singleton has completed initialization (you've awaited allReady() or isReady<T>()), you can access it like a regular singleton using get<T>() instead of getAsync<T>(). The async methods are only needed during the initialization phase or when accessing async factories.
// During startup - wait for initialization
await getIt.allReady();
// After ready - access normally (no await needed)
final database = getIt<Database>(); // Not getAsync!
final apiClient = getIt<ApiClient>();getAsync()
Retrieves an instance created by an async factory or waits for an async singleton to complete initialization.
Future<T> getAsync<T>({
String? instanceName,
dynamic param1,
dynamic param2,
Type? type,
})Example:
// Get async factory instance
final conn = await getIt.getAsync<DatabaseConnection>();
// Get async singleton (waits if still initializing)
final api = await getIt.getAsync<ApiClient>();
// Get named instance
final cache = await getIt.getAsync<CacheService>(instanceName: 'user-cache');
// Get with parameters (async factory param)
final report = await getIt.getAsync<Report>(
param1: 'user-123',
param2: DateTime.now(),
);Getting Multiple Async Instances
If you need to retrieve multiple async registrations of the same type, see the Multiple Registrations chapter for getAllAsync() documentation.
Dependency Management
Using dependsOn
The dependsOn parameter ensures initialization order. When you register a singleton with dependsOn, its factory function won't execute until all listed dependencies have signaled ready.
Example - Sequential initialization:
void configureDependencies() {
// 1. Config loads first (no dependencies)
getIt.registerSingletonAsync<ConfigService>(
() async {
final config = ConfigService();
await config.loadFromFile();
return config;
},
);
// 2. API client waits for config
getIt.registerSingletonAsync<ApiClient>(
() async {
final apiUrl = getIt<ConfigService>().apiUrl;
final client = ApiClient(apiUrl);
await client.authenticate();
return client;
},
dependsOn: [ConfigService],
);
// 3. Database waits for config
getIt.registerSingletonAsync<Database>(
() async {
final dbPath = getIt<ConfigService>().databasePath;
final db = Database(dbPath);
await db.initialize();
return db;
},
dependsOn: [ConfigService],
);
// 4. App model waits for everything
getIt.registerSingletonWithDependencies<AppModel>(
() => AppModel.withParams(
api: getIt<ApiClient>(),
db: getIt<Database>(),
config: getIt<ConfigService>()),
dependsOn: [ConfigService, ApiClient, Database],
);
}Sync Singletons with Dependencies
Sometimes you have a regular (sync) singleton that depends on other async singletons being ready first. Use registerSingletonWithDependencies for this pattern.
void registerSingletonWithDependencies<T>(
FactoryFunc<T> factoryFunc, {
String? instanceName,
required Iterable<Type>? dependsOn,
bool? signalsReady,
DisposingFunc<T>? dispose,
})Parameters:
factoryFunc- Sync function that creates the singleton instance (called after dependencies are ready)instanceName- Optional name to register multiple singletons of the same typedependsOn- List of types this singleton depends on (waits for them to be ready first)signalsReady- If true, you must manually callsignalReady()to mark as readydispose- Optional cleanup function called when unregistering or resetting
Example:
void configureDependencies() {
// Async singletons
getIt.registerSingletonAsync<ConfigService>(
() async => ConfigService.load(),
);
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient.create(),
);
// Sync singleton that depends on async singletons
getIt.registerSingletonWithDependencies<UserRepository>(
() => UserRepository(getIt<ApiClient>(), getIt<Database>()),
dependsOn: [ConfigService, ApiClient],
);
}
Future<void> main() async {
configureDependencies();
// Wait for all to be ready
await getIt.allReady();
// Now safe to access - dependencies are guaranteed ready
final userRepo = getIt<UserRepository>();
}Startup Orchestration
GetIt provides several functions to coordinate async initialization and wait for services to be ready.
allReady()
Returns a Future<void> that completes when all async singletons and singletons with signalsReady have completed their initialization.
Future<void> allReady({
Duration? timeout,
bool ignorePendingAsyncCreation = false,
})Parameters:
timeout- Optional timeout; throwsWaitingTimeOutExceptionif not ready in timeignorePendingAsyncCreation- If true, only waits for manual signals, ignores async singletons
Example with FutureBuilder:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// All services ready - show main app
return HomePage();
} else {
// Still initializing - show splash screen
return SplashScreen();
}
},
);
}
}Example with timeout:
Future<void> main() async {
setupDependencies();
try {
await getIt.allReady(timeout: Duration(seconds: 10));
runApp(MyApp());
} on WaitingTimeOutException catch (e) {
print('Initialization timeout!');
print('Not ready: ${e.notReadyYet}');
print('Already ready: ${e.areReady}');
print('Waiting chain: ${e.areWaitedBy}');
}
}Calling allReady() multiple times:
You can call allReady() multiple times. After the first allReady() completes, if you register new async singletons, you can await allReady() again to wait for the new ones.
// Register first batch of services
getIt.registerSingletonAsync<ConfigService>(() async => ConfigService.load());
getIt.registerSingletonAsync<Logger>(() async => Logger.initialize());
// Wait for first batch
await getIt.allReady();
print('Core services ready');
// Register second batch based on config
final config = getIt<ConfigService>();
if (config.enableFeatureX) {
getIt.registerSingletonAsync<FeatureX>(() async => FeatureX.initialize());
}
// Wait for second batch
await getIt.allReady();
print('All services ready');
runApp(MyApp());This pattern is especially useful with scopes where each scope needs its own initialization:
Future<void> main() async {
// Initialize base scope
await getIt.allReady();
// Push new scope with its own async services
getIt.pushNewScope(scopeName: 'user-session');
getIt.registerSingletonAsync<UserService>(() async => UserService.load());
// Wait for new scope to be ready
await getIt.allReady();
}isReady()
Returns a Future<void> that completes when a specific singleton is ready.
Future<void> isReady<T>({
Object? instance,
String? instanceName,
Duration? timeout,
Object? callee,
})Parameters:
T- Type of the singleton to wait forinstance- Alternatively, wait for a specific instance objectinstanceName- Wait for named registrationtimeout- Optional timeout; throwsWaitingTimeOutExceptionif not ready in timecallee- Optional parameter for debugging (helps identify who's waiting)
Example:
Future<void> main() async {
// setupDependencies(); // Setup your dependencies first
// Wait for specific service
await getIt.isReady<Database>();
// Now safe to use
final db = getIt<Database>();
// Wait for named instance
await getIt.isReady<ApiClient>(instanceName: 'production');
}isReadySync()
Checks if a singleton is ready without waiting (returns immediately).
bool isReadySync<T>({
Object? instance,
String? instanceName,
})Example:
void checkStatus() {
if (getIt.isReadySync<Database>()) {
print('Database is ready');
} else {
print('Database still initializing...');
}
}allReadySync()
Checks if all async singletons are ready without waiting.
bool allReadySync([bool ignorePendingAsyncCreation = false])Example:
void showUI() {
if (getIt.allReadySync()) {
// Show main UI
} else {
// Show loading indicator
}
}Named Dependencies with InitDependency
If you have named registrations, use InitDependency to specify both type and instance name.
void configureDependencies() {
// Register multiple API clients
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient.create('https://api-v1.example.com'),
instanceName: 'api-v1',
);
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient.create('https://api-v2.example.com'),
instanceName: 'api-v2',
);
// Depend on specific named instance
getIt.registerSingletonWithDependencies<DataSync>(
() => DataSync(getIt<ApiClient>(instanceName: 'api-v2')),
dependsOn: [InitDependency(ApiClient, instanceName: 'api-v2')],
);
}Manual Ready Signaling
Sometimes you need more control over when a singleton signals it's ready. This is useful when initialization involves multiple steps or callbacks.
Using signalsReady Parameter
When you set signalsReady: true during registration, GetIt won't automatically mark the singleton as ready. You must manually call signalReady().
Example:
class ConfigService {
bool isReady = false;
ConfigService() {
_initialize();
}
Future<void> _initialize() async {
// Complex async initialization
await loadRemoteConfig();
await validateConfig();
await setupConnections();
isReady = true;
// Signal that we're ready
GetIt.instance.signalReady(this);
}
Future<void> loadRemoteConfig() async {}
Future<void> validateConfig() async {}
Future<void> setupConnections() async {}
}
void configureDependencies() {
getIt.registerSingleton<ConfigService>(
ConfigService(),
signalsReady: true, // Must manually signal ready
);
}
Future<void> main() async {
configureDependencies();
// Wait for ready signal
await getIt.isReady<ConfigService>();
}Using WillSignalReady Interface
Instead of passing signalsReady: true, implement the WillSignalReady interface. GetIt automatically detects this and waits for manual signaling.
class ConfigService implements WillSignalReady {
bool isReady = false;
ConfigService() {
_initialize();
}
Future<void> _initialize() async {
await loadConfig();
isReady = true;
GetIt.instance.signalReady(this);
}
Future<void> loadConfig() async {}
}
void configureDependencies() {
// No signalsReady parameter needed - interface handles it
getIt.registerSingleton<ConfigService>(ConfigService());
}signalReady()
Manually signals that a singleton is ready.
void signalReady(Object? instance)Parameters:
instance- The instance that's ready (passingnullis legacy and not recommended)
Example:
class DatabaseService {
DatabaseService() {
_init();
}
Future<void> _init() async {
await connectToDatabase();
await runMigrations();
// Signal this instance is ready
GetIt.instance.signalReady(this);
}
Future<void> connectToDatabase() async {}
Future<void> runMigrations() async {}
}Legacy Feature
signalReady(null) (global ready signal without an instance) is a legacy feature from earlier versions of GetIt. It's recommended to use async registrations (registerSingletonAsync, etc.) or instance-specific signaling instead. The global signal approach is less clear about what's being initialized and doesn't integrate well with dependency management.
Note: The global signalReady(null) will throw an error if you have any async registrations or instances with signalsReady: true that haven't signaled yet. Instance-specific signaling works fine alongside async registrations.
Best Practices
1. Prefer registerSingletonAsync for App Initialization
For services needed at app startup, use registerSingletonAsync (not lazy) so they start initializing immediately.
void configureDependencies() {
// Good - starts initializing immediately
getIt.registerSingletonAsync<Database>(() async => Database.connect());
// Less ideal - won't initialize until first access
getIt.registerLazySingletonAsync<Database>(() async => Database.connect());
}2. Use dependsOn to Express Dependencies
Let GetIt manage initialization order instead of manually orchestrating with isReady().
// Good - clear dependency chain
void configureDependencies() {
getIt.registerSingletonAsync<ConfigService>(() async => ConfigService.load());
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient(getIt<ConfigService>().apiUrl),
dependsOn: [ConfigService],
);
}
// Less ideal - manual orchestration
void configureDependenciesManual() {
getIt.registerSingletonAsync<ConfigService>(() async => ConfigService.load());
getIt.registerSingletonAsync<ApiClient>(() async {
await getIt.isReady<ConfigService>(); // Manual waiting
return ApiClient(getIt<ConfigService>().apiUrl);
});
}3. Use FutureBuilder for Splash Screens
Display a loading screen while services initialize.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return ErrorScreen(snapshot.error!);
}
if (snapshot.hasData) {
return HomePage();
}
return SplashScreen();
},
),
);
}
}4. Always Set Timeouts for allReady()
Prevent your app from hanging indefinitely if initialization fails.
Future<void> main() async {
try {
await getIt.allReady(timeout: Duration(seconds: 30));
runApp(MyApp());
} on WaitingTimeOutException catch (e) {
// Handle timeout - log error, show error screen, etc.
runApp(ErrorApp(error: e));
}
}Common Patterns
Pattern 1: Layered Initialization
void configureDependencies() {
// Layer 1: Core infrastructure
getIt.registerSingletonAsync<ConfigService>(() async => ConfigService.load());
getIt.registerSingletonAsync<Logger>(() async => Logger.initialize());
// Layer 2: Network and data access
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient(getIt<ConfigService>().apiUrl),
dependsOn: [ConfigService],
);
getIt.registerSingletonAsync<Database>(
() async => Database(getIt<ConfigService>().dbPath),
dependsOn: [ConfigService],
);
// Layer 3: Business logic
getIt.registerSingletonWithDependencies<UserRepository>(
() => UserRepository(getIt<ApiClient>(), getIt<Database>()),
dependsOn: [ApiClient, Database],
);
// Layer 4: Application state
getIt.registerSingletonWithDependencies<AppModel>(
() => AppModel(getIt<UserRepository>()),
dependsOn: [UserRepository],
);
}Pattern 2: Conditional Initialization
void configureDependencies({required bool isProduction}) {
getIt.registerSingletonAsync<ConfigService>(
() async => ConfigService.load(),
);
if (isProduction) {
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient(getIt<ConfigService>().prodUrl),
dependsOn: [ConfigService],
);
} else {
getIt.registerSingletonAsync<ApiClient>(
() async => MockApiClient(),
dependsOn: [ConfigService],
);
}
}Pattern 3: Progress Tracking
class InitializationProgress extends ChangeNotifier {
final Map<String, bool> _progress = {};
void markReady(String serviceName) {
_progress[serviceName] = true;
notifyListeners();
}
double get percentComplete =>
_progress.values.where((ready) => ready).length / _progress.length;
}
void configureDependencies(InitializationProgress progress) {
getIt.registerSingletonAsync<ConfigService>(
() async => ConfigService.load(),
onCreated: (_) => progress.markReady('Config'),
);
getIt.registerSingletonAsync<Database>(
() async => Database.connect(),
dependsOn: [ConfigService],
onCreated: (_) => progress.markReady('Database'),
);
getIt.registerSingletonAsync<ApiClient>(
() async => ApiClient.create(),
dependsOn: [ConfigService],
onCreated: (_) => progress.markReady('API'),
);
}Pattern 4: Retry on Failure
Future<T> withRetry<T>(
Future<T> Function() operation, {
int maxAttempts = 3,
}) async {
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (e) {
if (attempt == maxAttempts) rethrow;
await Future.delayed(Duration(seconds: attempt));
}
}
throw StateError('Should never reach here');
}
void configureDependencies() {
getIt.registerSingletonAsync<ApiClient>(
() => withRetry(() async => ApiClient.connect()),
);
}Further Reading
- Detailed blog post on async factories and startup orchestration
- Scopes Documentation - Async initialization within scopes
- Testing Documentation - Mocking async services in tests