levit_reactive

Pub Version Platforms License: MIT codecov

Purpose & Scope

levit_reactive is Levit's pure Dart reactive runtime.

This package is responsible for:

  • Mutable and immutable reactive values.
  • Derived state (LxComputed, async computed variants).
  • Side-effect workers (LxWorker family).
  • Keyed reactive resources (LxFamily).
  • Deterministic propagation and batch semantics.
  • Reactive middleware interception.

This package does not include:

  • Dependency injection and lifecycle container semantics (levit_scope).
  • Flutter widget bindings (levit_flutter_core, levit_flutter).

Conceptual Overview

Reactive objects expose values and notify observers. Observers subscribe implicitly (through dependency tracking) or explicitly (listeners/workers).

Core abstractions:

  • .lx extensions create reactive sources.
  • LxComputed defines derived values from dependencies.
  • LxWorker runs side-effects when dependencies change.
  • Lx.batch / Lx.batchAsync coalesce propagation.

Getting Started

dependencies:
  levit_reactive: ^latest
import 'package:levit_reactive/levit_reactive.dart';

void main() {
  final count = 0.lx;
  final doubled = LxComputed(() => count() * 2);

  final worker = LxWorker(doubled, (value) {
    print('doubled=$value');
  });

  count(1);
  count(2);

worker.close();
doubled.close();
count.close();
}

Async Concurrency

Async computed values and workers accept LxAsyncConcurrency:

  • latest keeps the existing behavior: invalidations may overlap and only the latest result is published.
  • exhaustLatest runs one computation at a time and coalesces any changes that arrive while it runs into exactly one trailing computation.
final results = LxComputed.async(
  () => search(query()),
  concurrency: LxAsyncConcurrency.exhaustLatest,
);

Re-bindable Streams

LxStreamCompletionPolicy.close remains the default. Use retain when a completed source should preserve its last status and be explicitly rebound:

final updates = LxStream<int>.idle(
  completionPolicy: LxStreamCompletionPolicy.retain,
);

updates.restartDeferred(() => repository.watchUpdates());

Keyed Reactive Families

LxFamily<K, R> lazily creates and caches one reactive resource per key:

final products = LxFamily<String, LxStream<Product>>(
  (sku) => LxStream.defer(() => repository.watchProduct(sku)),
  name: 'products',
  eviction: const LxFamilyEviction.whenInactive(
    gracePeriod: Duration(seconds: 30),
  ),
);

final selectedProduct = products(selectedSku);
products.invalidate(selectedSku);
products.close();

Raw keys are hashed in diagnostic names unless an application-controlled debugKey is supplied, so high-cardinality or sensitive key values are not exported by default.

Middleware Lifecycle (Token-Based)

import 'package:levit_reactive/levit_reactive.dart';

const historyToken = #state_history;

void configure() {
  Lx.addMiddleware(LevitReactiveHistoryMiddleware(), token: historyToken);
}

void teardown() {
  Lx.removeMiddlewareByToken(historyToken);
}

Design Principles

  • Ordered, deterministic propagation.
  • Fine-grained dependency tracking.
  • Explicit lifecycle closure for long-lived resources.
  • Predictable middleware interception.

Libraries

levit_reactive
Pure Dart reactive runtime for state propagation and derivation.