Skip to content

k-arindam/initializable.dart

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

9 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Initializable β€” Zero-Boilerplate Async Initialization Gating for Dart

Pub Version Dart License: MIT

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.0

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() or markFailed() 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.dart files.
  • Single-threaded safety β€” Dart's event loop guarantees sequential access within an isolate.
  • Completer-based gating β€” Multiple Future listeners on a single Completer.future all 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.0

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

About

Zero-Boilerplate Async Initialization Gating for Dart Classes

Topics

Resources

License

Code of conduct

Stars

2 stars

Watchers

0 watching

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Languages