Stop scattering if (!isReady) return; checks everywhere. Gate your async methods behind initialization β automatically.
Initializable ensures that your class's async methods automatically suspend until asynchronous setup is complete. No more runtime crashes from uninitialized state. Works with any Dart class.
Classes often require asynchronous setup before they're ready β connecting to a database, loading configuration, or authenticating with a remote server. Every method that depends on this setup must somehow wait until it's done.
class DatabaseService {
bool _isReady = false;
Future<List<Row>> query(String sql) async {
// π© You have to remember this everywhere
while (!_isReady) {
await Future.delayed(Duration(milliseconds: 10));
}
return await db.execute(sql);
}
Future<void> insert(Row row) async {
// π© Miss one and you get a runtime crash
while (!_isReady) {
await Future.delayed(Duration(milliseconds: 10));
}
await db.insert(row);
}
}This is tedious, error-prone, and doesn't scale.
Initializable gives you a guarded() wrapper. Every guarded method automatically waits for initialization to complete!
class DatabaseService with Initializable {
Future<void> setup() async {
await connectToDatabase();
markInitialized(); // π Gate opens β all waiting methods proceed!
}
// β
Automatically waits for setup() β ZERO boilerplate
Future<List<Row>> query(String sql) => guarded(() async {
return await db.execute(sql);
});
Future<void> insert(Row row) => guarded(() async {
await db.insert(row);
});
}Zero runtime overhead after initialization. Zero chance of forgetting a check.
dependencies:
initializable: ^1.0.0import 'package:initializable/initializable.dart';
class MyService with Initializable {
Future<void> setup() async {
await performAsyncWork();
markInitialized(); // π Done!
}
Future<Data> fetchData() => guarded(() async {
return await loadData(); // β¨ Automatically waits for setup()
});
}final service = MyService();
// Kick off setup concurrently
service.setup();
// This will safely wait until setup() completes!
final data = await service.fetchData();Initializable uses a gate pattern: an internal Completer<void> suspends async callers until markInitialized() is called.
| Concept | Description |
|---|---|
𧬠Mixin (Initializable) |
Provides markInitialized(), guarded(), awaitInitialized(), and initialized. |
π§ Gate (InitializationGate) |
Holds the Completer and manages state transitions. |
π‘οΈ Guarded (guarded()) |
Wraps an async action to wait for initialization first. |
𧬠Throwing Mixin (ThrowingInitializable) |
Adds markFailed() for failable initialization. |
π§ Throwing Gate (ThrowingInitializationGate) |
Supports success/failure with error propagation. |
| Component | Non-Throwing | Throwing (Failable) |
|---|---|---|
| Mixin | Initializable |
ThrowingInitializable |
| Gate | InitializationGate |
ThrowingInitializationGate |
Use this when your setup cannot fail (e.g., loading a local cache).
import 'package:initializable/initializable.dart';
class CacheService with Initializable {
late Map<String, Data> _store;
Future<void> warmUp() async {
_store = await loadFromDisk();
markInitialized(); // π Gate opens
}
// β
Guarded β waits for warmUp()
Future<Data?> get(String key) => guarded(() async {
return _store[key];
});
// β Sync β no gating needed
String get cacheDir => '/tmp/cache';
}Caller 1 β guarded(get("key")) β Gate: pending β SUSPEND
Caller 2 β guarded(get("key")) β Gate: pending β SUSPEND
markInitialized() β Gate: initialized
Caller 1 β resume β
β returns _store["key"]
Caller 2 β resume β
β returns _store["key"]
Future calls β Gate: initialized β proceed immediately (zero overhead)
Use this when your setup can fail (e.g., network connections, database migrations).
import 'package:initializable/initializable.dart';
class DatabaseService with ThrowingInitializable {
late DbConnection _connection;
Future<void> connect(String url) async {
try {
_connection = await DbConnection.open(url);
markInitialized(); // β
Success
} catch (e, st) {
markFailed(e, st); // β Propagate error to all waiters
}
}
// β
Waits for connect(), or throws if it failed
Future<List<Row>> query(String sql) => guarded(() async {
return await _connection.execute(sql);
});
} ββββββββββββββββββββ
β Pending β
ββββββββββ¬ββββββββββ
β
βββββββββββββ΄ββββββββββββ
β β
markInitialized() markFailed(error)
β β
βΌ βΌ
ββββββββββββββββ βββββββββββββββββ
β Initialized β β Failed β
ββββββββββββββββ βββββββββββββββββ
guarded() β runs guarded() β throws
immediately stored error
State Stickiness: The first call to
markInitialized()ormarkFailed()wins. Subsequent calls to either method are safe no-ops.
If you prefer fine-grained control, use awaitInitialized() directly instead of guarded():
class SelectiveService with Initializable {
Future<void> setup() async {
await loadResources();
markInitialized();
}
// Manual gating β only this method waits
Future<Result> criticalOperation() async {
await awaitInitialized();
return performWork();
}
// No gating β caller responsible for timing
Future<Result?> bestEffortOperation() async {
return tryPerformWork();
}
}| Member | Type | Description |
|---|---|---|
initialized |
bool (getter) |
Whether markInitialized() has been called. |
markInitialized() |
void |
Opens the gate. Idempotent. |
guarded<T>(action) |
Future<T> |
Executes action after initialization. |
awaitInitialized() |
Future<void> |
Raw suspension point. Prefer guarded(). |
| Member | Type | Description |
|---|---|---|
initialized |
bool (getter) |
true only on success. |
isResolved |
bool (getter) |
true after success or failure. |
markInitialized() |
void |
Opens the gate. Idempotent + sticky. |
markFailed(error, [st]) |
void |
Fails the gate. Idempotent + sticky. |
guarded<T>(action) |
Future<T> |
Executes action or throws. |
awaitInitialized() |
Future<void> |
Raw suspension. Throws on failure. |
| Member | Description |
|---|---|
initialized |
Whether the gate is open. |
markInitialized() |
Opens the gate. Idempotent. |
wait() |
Suspends until the gate opens. |
| Member | Description |
|---|---|
initialized |
true only on success. |
isResolved |
true after success or failure. |
markInitialized() |
Opens the gate. Idempotent + sticky. |
markFailed(error, [st]) |
Fails the gate. Idempotent + sticky. |
wait() |
Suspends until resolved; throws on failure. |
lib/
βββ initializable.dart # Barrel export
βββ src/
βββ initialization_gate.dart # InitializationGate (non-throwing)
βββ throwing_initialization_gate.dart # ThrowingInitializationGate
βββ initializable_mixin.dart # Initializable mixin
βββ throwing_initializable_mixin.dart # ThrowingInitializable mixin
- Zero dependencies β Only depends on
dart:async(core SDK). - No code generation β No
build_runner, no.g.dartfiles. - Single-threaded safety β Dart's event loop guarantees sequential access within an isolate.
- Completer-based gating β Multiple
Futurelisteners on a singleCompleter.futureall resume together.
This package is a Dart port of the Swift Initializable package, which uses Swift Macros for compile-time code injection. Since Dart lacks macros, the guarded() wrapper pattern provides equivalent runtime behavior.
Add to your pubspec.yaml:
dependencies:
initializable: ^1.0.0Then run:
dart pub get| Tool | Minimum Version |
|---|---|
| Dart SDK | 3.12.2 |
This project is available under the MIT License. See the LICENSE file for details.
Built with β€οΈ for the Dart ecosystem
