
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.
π The Problem
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.
The Tedious Way (Without Initializable)
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.
β¨ The Solution
Initializable gives you a guarded() wrapper. Every guarded method automatically waits for initialization to complete!
The Elegant Way (With Initializable)
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.
π Table of Contents
π Quick Start
1. Add the Package
dependencies:
initializable: ^1.0.1
2. Mix In & Use
import '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()
});
}
3. Call It
final service = MyService();
// Kick off setup concurrently
service.setup();
// This will safely wait until setup() completes!
final data = await service.fetchData();
π§ Core Concepts
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. |
Variants
| Component | Non-Throwing | Throwing (Failable) |
|---|---|---|
| Mixin | Initializable |
ThrowingInitializable |
| Gate | InitializationGate |
ThrowingInitializationGate |
π Usage Guide
1. Non-Throwing Initialization
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';
}
What Happens at Runtime
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)
2. Throwing Initialization (Failable)
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);
});
}
State Machine
ββββββββββββββββββββ
β 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.
3. Manual Per-Method Control
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();
}
}
π API Reference
Mixins
Initializable
| 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(). |
ThrowingInitializable
| 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. |
Gates
InitializationGate
| Member | Description |
|---|---|
initialized |
Whether the gate is open. |
markInitialized() |
Opens the gate. Idempotent. |
wait() |
Suspends until the gate opens. |
ThrowingInitializationGate
| 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. |
π Architecture
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
Design Principles
- 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.
Ported from Swift
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.
π¦ Installation
Add to your pubspec.yaml:
dependencies:
initializable: ^1.0.1
Then run:
dart pub get
βοΈ Requirements
| Tool | Minimum Version |
|---|---|
| Dart SDK | 3.12.2 |
π License
This project is available under the MIT License. See the LICENSE file for details.
Built with β€οΈ for the Dart ecosystem
Libraries
- initializable
- Zero-boilerplate async initialization gating for Dart.