Capsa

Capsa

Capsa is a reactive state management library for Flutter, inspired by SolidJS. It provides fine-grained reactivity with Signals, Computed values, Effects, and a rich set of reactive widgets — all without the boilerplate of ChangeNotifier, BLoC, or Riverpod.

Capsa builds on top of flutter_rearch for capsule-based dependency injection and widget lifecycle management. RearchConsumer, WidgetHandle, and capsule() are re-exported directly — you only need import 'package:capsa/capsa.dart'.

English | فارسی | Azərbaycanca

⚠️ Alpha Notice: Capsa is under active development. The API may change. Not recommended for production use.

Features

  • Fine-grained ReactivitySignal<T> / Computed<T> / Effect with push-based propagation, glitch-free, lazy evaluation
  • Batchingbatch() groups multiple signal writes into a single notification
  • ReactiveList — Observable List<T> backed by a version signal; works with UltraFor
  • Reactive WidgetsXReactive, UltraBuilder, UltraObserver rebuild only when tracked signals change
  • X Helper APIX.text, X.show, X.opacity, X.container, X.button, X.transition — declarative reactive helpers
  • Async ResourcesCapsaResource<T> with reactive loading/error/data states & XSuspense widget
  • MVVM ArchitectureScreenModel, Business, Repository, Datasource with lifecycle management
  • Dependency InjectionUltraDI with scoped singletons, factories, async singletons, and circular dependency detection
  • Animated TransitionsXTransition drives widget animations directly from a Signal<double>
  • Low-level Render WidgetsUltraReactiveBox, UltraReactiveFlex, UltraReactiveStack, ReactiveText — reactive at the render-object level
  • Reactive CollectionsUltraFor, UltraGrid, UltraWrap, UltraCollection render ReactiveList data
  • Code Generator@Capsa(path) annotation generates capsule wiring and feature scaffolding
  • Logging & DevToolsCapsaLogger with categories & ring buffer; reactive graph inspector for leak detection
  • Router IntegrationReactiveRouteObserver exposes the current route as a Signal

Getting Started

Add dependency

dependencies:
  capsa: ^1.0.0

rearch and flutter_rearch are included automatically — no need to add them separately.

Import

import 'package:capsa/capsa.dart';

Everything is available from a single import: RearchConsumer, WidgetHandle, capsule(), Signal, Computed, XReactive, UltraFor, and all other APIs.


Quick Start

import 'package:flutter/material.dart';
import 'package:capsa/capsa.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatelessWidget {
  // A reactive signal — reading triggers tracking, writing notifies dependents
  final count = Signal(0);
  // A derived value — auto-updates when its dependencies change
  late final doubled = Computed(() => count() * 2);

  CounterScreen() {
    // Effects run automatically and re-run when tracked signals change
    effect(() {
      CapsaLogger.info(CapsaLogCategory.effect, 'Count is now ${count()}');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Capsa Demo')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // XReactive rebuilds only when the signals it reads change
            XReactive(() => Text('Count: ${count()}')),
            XReactive(() => Text('Doubled: ${doubled()}')),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => count.value = count() + 1,
              child: const Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

Core Concepts

Signal

final name = Signal('Alice');
print(name());     // Read + track → 'Alice'
name.value = 'Bob'; // Write → notifies dependents

Computed

final first = Signal('John');
final last = Signal('Doe');
final full = Computed(() => '${first()} ${last()}');

print(full()); // 'John Doe'
last.value = 'Smith';
print(full()); // 'John Smith' — auto-recomputed

Effect

effect(() {
  print('Name changed to ${name()}'); // re-runs whenever name changes
}, debugLabel: 'nameWatcher');

// Dispose manually if not using a ReactiveScope:
final fx = effect(() { ... });
fx.dispose();

Batch

ReactiveScheduler.batch(() {
  first.value = 'Jane';
  last.value = 'Doe';  // only one notification after the batch
});

Reactive Widgets

XReactive

Rebuilds when any signal read inside the builder changes:

XReactive(
  () => Column(
    children: [
      Text('Name: ${user.name()}'),
      Text('Age: ${user.age()}'),
    ],
  ),
)

UltraBuilder

Same concept, passes BuildContext:

UltraBuilder(
  builder: (ctx) {
    final user = model.user();
    return Text('Hello, ${user.name}');
  },
)

UltraObserver

Runs a builder and rebuilds when tracked signals change. Useful for fine-grained parts of the tree:

UltraObserver(
  builder: (_) {
    final hasError = model.error() != null;
    return hasError ? const Icon(Icons.warning) : const SizedBox.shrink();
  },
)

XSuspense

Handles the loading → ready → error lifecycle of a CapsaResource:

XSuspense<Map<String, int>>(
  resource: statsResource,
  fallback: const CircularProgressIndicator(),
  onError: (err, _) => TextButton(
    onPressed: reload,
    child: Text('Retry ($err)'),
  ),
  builder: (ctx, stats) => Text('Total: ${stats['total']}'),
)

XTransition

Animates a widget based on a Signal<double>:

XTransition(
  value: headerOpacity,
  duration: const Duration(milliseconds: 600),
  curve: Curves.easeInOut,
  builder: (ctx, opacity) => Opacity(
    opacity: opacity,
    child: Container(color: Colors.blue, height: 100),
  ),
)

UltraFor

Renders a ReactiveList as a SliverList:

CustomScrollView(
  slivers: [
    UltraFor<UserModel>(
      list: state.users,
      builder: (user, index) => ListTile(
        title: Text(user.name),
        subtitle: Text(user.email),
      ),
    ),
  ],
)

X Helper API

Convenient declarative wrappers for common widget patterns:

X.text(nameSignal)                          // Reactive Text widget
X.show(visibleSignal, child)                // Conditional visibility
X.opacity(opacitySignal, child)             // Reactive opacity
X.container(color: colorSignal, child: ...) // Reactive container
X.button(child: ..., onTap: handleTap)      // Reactive button
X.transition(value: signal, builder: ...)   // Reactive transition

ReactiveList

An observable list that integrates with UltraFor:

final items = ReactiveList<String>();

items.add('apple');
items.addAll(['banana', 'cherry']);
items.removeAt(0);
items[0] = 'blueberry';
items.batch((list) {
  list.add('one');
  list.add('two'); // single notification
});

Read the current value (tracks dependency):

XReactive(() => Text('Count: ${items.length}'));
XReactive(() => Text('Items: ${items.value.join(', ')}'));

Async Resources

final resource = CapsaResource(() => fetchApiData());

// Reactive state
print(resource.status());   // ResourceStatus.loading / .ready / .error
print(resource.data());     // T? — the result when ready
print(resource.error());    // Object? — the error if failed

// Retry
await resource.reload();

MVVM Pattern

Capsa follows a layered MVVM architecture where each layer is wired via rearch capsules. The @Capsa annotation (see Code Generator) generates capsule wiring code automatically.

Architecture layers

Layer Base class Responsibility
ScreenModel ScreenModel View state, business orchestration, lifecycle
Business Business Use cases, validation, business logic
Repository Repository Data access abstraction, caching strategy
Datasource Datasource Raw API/DB calls, network or local storage
State (plain class) Reactive fields (Signal, ReactiveList, Computed)
View RearchConsumer Flutter widgets with capsule DI access
// State — reactive fields only
class MyState {
  final users = ReactiveList<UserModel>();
  final isLoading = Signal<bool>(true);
  late final userCount = Computed(() => users.length);
}

// Business — use cases
class MyBusiness extends Business {
  final Repository repository;
  MyBusiness({required this.repository});

  Future<List<User>> loadUsers() async { ... }
}

// ScreenModel — orchestrator, extends ReactiveScope for auto-disposal
class MyScreenModel extends ScreenModel {
  final state = MyState();
  final MyBusiness business;

  MyScreenModel({required this.business});

  @override
  void onInit() {
    loadData();
    // auto-disposed when ScreenModel is disposed
    track(effect(() { ... }));
  }

  Future<void> loadData() async { ... }
}

// Screen — uses RearchConsumer to access capsules
class MyScreen extends RearchConsumer {
  const MyScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetHandle use) {
    final model = use(myScreenModelCapsule);
    return XReactive(() => Text(model.state.userCount()));
  }
}

Capsule wiring (hand-written)

final myDatasourceCapsule = capsule((_) => MyDatasource());
final myRepositoryCapsule = capsule((use) =>
    MyRepository(use(myDatasourceCapsule)));
final myBusinessCapsule = capsule((use) =>
    MyBusiness(repository: use(myRepositoryCapsule)));
final myScreenModelCapsule = capsule((use) =>
    MyScreenModel(business: use(myBusinessCapsule)));

When using @Capsa, these capsules are generated automatically — see the Code Generator section below.


Dependency Injection (UltraDI)

final di = UltraDI();

di.registerSingleton<ApiClient>(ApiClient());
di.registerFactory<Repository>((d) => Repository(d.get<ApiClient>()));
di.registerLazySingleton<Service>((d) => Service(d.get<Repository>()));

final service = di.get<Service>();

Scoped DI:

di.pushScope();  // new child scope
di.registerFactory<ScreenModel>(...); // scoped
di.popScope();   // disposed with scope

Code Generator

Capsa provides two code generation tools:

Tool Purpose
@Capsa annotation + build_runner Generates .capsa.dart capsule wiring files
dart run capsa CLI Scaffolds a full feature folder structure with template files

@Capsa annotation + build_runner

Place the annotation on any class inside your feature folder:

import 'package:capsa/capsa.dart';

@Capsa(path: 'lib/screen/profile')
class Profile {}

Setup: Add to your project's build.yaml:

targets:
  $default:
    builders:
      capsa|feature_builder:
        enabled: true

Run the generator:

dart run build_runner build

What it generatesprofile.capsa.dart:

// GENERATED CODE - DO NOT MODIFY BY HAND

import 'package:rearch/rearch.dart';
import 'business/profile_business.dart';
import 'repository/profile_repository.dart';
import 'datasource/profile_datasource.dart';

final profileBusinessCapsule = capsule((use) {
  return ProfileBusiness(repository: use(profileRepositoryCapsule));
});

final profileRepositoryCapsule = capsule((use) {
  return ProfileRepository(use(profileDatasourceCapsule));
});

final profileDatasourceCapsule = capsule((use) {
  return ProfileDatasource();
});

The generated capsules follow a strict layering: DatasourceCapsuleRepositoryCapsuleBusinessCapsule

When you also define a ScreenModel and register it in the feature file, you can add:

final profileScreenModelCapsule = capsule((use) {
  return ProfileScreenModel(business: use(profileBusinessCapsule));
});

These capsules are consumed by RearchConsumer screens:

class ProfileScreen extends RearchConsumer {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetHandle use) {
    final model = use(profileScreenModelCapsule);
    return XReactive(() => Text(model.state.userCount()));
  }
}

CLI feature scaffolder

Scaffold a complete feature folder with a single command:

# Format: dart run capsa <feature-name> <target-path>
dart run capsa profile lib/screen/profile

# Or when the folder name matches the feature:
dart run capsa lib/screen/profile

Creates the following structure:

lib/screen/profile/
├── profile.dart                          # @Capsa annotation + re-exports
├── profile.capsa.dart                    # generated capsule wiring (after build_runner)
├── business/
│   └── profile_business.dart
├── repository/
│   └── profile_repository.dart
├── datasource/
│   └── profile_datasource.dart
├── model/
│   └── profile_model.dart
├── state/
│   └── profile_state.dart
├── screen_model/
│   └── profile_screen_model.dart
└── view/
    └── profile_screen.dart

Each template file has a minimal starting implementation ready for you to fill in.


Architecture

┌─────────────────────────────────────────────────────────┐
│                      Presentation                        │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────────┐  │
│  │ XReactive│  │UltraFor  │  │ XSuspense/XTransition│  │
│  └────┬─────┘  └────┬─────┘  └──────────┬───────────┘  │
│       │              │                   │              │
│  ┌────▼──────────────▼───────────────────▼───────────┐  │
│  │               Reactive Widgets                     │  │
│  └───────────────────────┬───────────────────────────┘  │
│                          │                              │
│  ┌───────────────────────▼───────────────────────────┐  │
│  │              ScreenModel (MVVM)                    │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────────────┐   │  │
│  │  │  State   │ │ Business │ │  CapsaResource   │   │  │
│  │  └──────────┘ └────┬─────┘ └──────────────────┘   │  │
│  └───────────────────────┬───────────────────────────┘  │
│                          │                              │
│  ┌───────────────────────▼───────────────────────────┐  │
│  │              Core Reactive Engine                   │  │
│  │  ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────────┐  │  │
│  │  │Signal│ │Computed│ │ Effect │ │ ReactiveList │  │  │
│  │  └──────┘ └────────┘ └────────┘ └──────────────┘  │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │   Scheduler (batch, priority queue, flush)   │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  └─────────────────────────────────────────────────────┘  │
│                          │                              │
│  ┌───────────────────────▼───────────────────────────┐  │
│  │              UltraDI Container                      │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────────────┐   │  │
│  │  │Singleton │ │ Factory  │ │    Scopes         │   │  │
│  │  └──────────┘ └──────────┘ └──────────────────┘   │  │
│  └─────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Additional Widgets

Widget Description
UltraGrid<T> Reactive grid layout backed by ReactiveList
UltraWrap<T> Reactive wrap layout backed by ReactiveList
UltraCollection<T> Auto-selects list/grid/wrap based on data size
UltraReactiveListView<T> Reactive ListView backed by Signal<List<T>>
UltraReactiveBox Render-object level container with reactive props
UltraReactiveFlex Render-object level flex with reactive direction/gap
UltraReactiveStack Render-object level stack with reactive alignment
ReactiveText Render-object level text driven by Signal<String>
UltraReactiveOpacity Render-object level opacity driven by Signal<double>
UltraReactiveAnimatedBox Animated scale widget driven by a Signal<double>

License

MIT

Libraries

capsa