optireact 1.0.0 copy "optireact: ^1.0.0" to clipboard
optireact: ^1.0.0 copied to clipboard

A lightweight, high-performance reactive state management library for Flutter. Zero external dependencies, auto-dispose, async handling, undo/redo, rate limiting, and more.

example/lib/main.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:optireact/optireact.dart';

// -- Scope: groups all demo notifiers with auto-dispose ----------------------
class DemoScope extends ReactiveScope {
  late final counter = reactive(0);
  late final price = reactive(10.0);
  late final quantity = reactive(2);
  late final total = create(ReactiveComputedNotifier<double>(
    dependencies: [price, quantity],
    compute: () => price.value * quantity.value,
  ));
  late final asyncUser = asyncReactive<String>();
  late final undoable = create(UndoableNotifier<int>(0, maxHistory: 20));
  late final search = create(
    ReactiveDebouncedNotifier<String>('',
        duration: const Duration(milliseconds: 400)),
  );
  late final searchResults = reactiveList<String>();
  late final favorites = reactiveSet<String>();
}

// -- Entry point -------------------------------------------------------------
void main() => runApp(const OptiReactExampleApp());

class OptiReactExampleApp extends StatelessWidget {
  const OptiReactExampleApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'OptiReact Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
      ),
      home: ReactiveHost<DemoScope>(
        create: DemoScope.new,
        builder: (context, scope, child) => _Home(scope: scope),
      ),
    );
  }
}

// -- Home with tabs ----------------------------------------------------------
class _Home extends StatelessWidget {
  const _Home({required this.scope});
  final DemoScope scope;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 5,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('OptiReact Demo'),
          bottom: const TabBar(isScrollable: true, tabs: [
            Tab(icon: Icon(Icons.add), text: 'Counter'),
            Tab(icon: Icon(Icons.calculate), text: 'Computed'),
            Tab(icon: Icon(Icons.cloud_download), text: 'Async'),
            Tab(icon: Icon(Icons.undo), text: 'Undo'),
            Tab(icon: Icon(Icons.search), text: 'Search'),
          ]),
        ),
        body: TabBarView(children: [
          _CounterTab(counter: scope.counter),
          _ComputedTab(p: scope.price, q: scope.quantity, t: scope.total),
          _AsyncTab(notifier: scope.asyncUser),
          _UndoTab(notifier: scope.undoable),
          _SearchTab(search: scope.search, results: scope.searchResults),
        ]),
      ),
    );
  }
}

// -- 1. Counter: Reactive + batch --------------------------------------
class _CounterTab extends StatelessWidget {
  const _CounterTab({required this.counter});
  final ReactiveNotifier<int> counter;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Reactive<int>(
        notifier: counter,
        autoDispose: false,
        builder: (ctx, value, _) => Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$value', style: ctx.textTheme.displayLarge),
            const SizedBox(height: 24),
            Row(mainAxisSize: MainAxisSize.min, children: [
              FilledButton.tonalIcon(
                onPressed: () => counter.value--,
                icon: const Icon(Icons.remove),
                label: const Text('Decrement'),
              ),
              const SizedBox(width: 16),
              FilledButton.icon(
                onPressed: () => counter.value++,
                icon: const Icon(Icons.add),
                label: const Text('Increment'),
              ),
            ]),
            const SizedBox(height: 16),
            OutlinedButton(
              onPressed: () => ReactiveNotifier.batch(() {
                counter.value += 5;
                counter.value += 5;
              }),
              child: const Text('Batch +10 (single rebuild)'),
            ),
            const SizedBox(height: 8),
            TextButton(
              onPressed: counter.reset,
              child: const Text('Reset'),
            ),
          ],
        ),
      ),
    );
  }
}

// -- 2. Computed: price * quantity = total ------------------------------------
class _ComputedTab extends StatelessWidget {
  const _ComputedTab({required this.p, required this.q, required this.t});
  final ReactiveNotifier<double> p;
  final ReactiveNotifier<int> q;
  final ReactiveComputedNotifier<double> t;

  @override
  Widget build(BuildContext context) {
    final tt = context.textTheme;
    return Padding(
      padding: const EdgeInsets.all(24),
      child: ReactiveMulti(
        notifiers: [p, q, t],
        autoDispose: false,
        builder: (ctx, _) => Column(mainAxisSize: MainAxisSize.min, children: [
          Text('Shopping Cart', style: tt.headlineSmall),
          const SizedBox(height: 32),
          _row(
              ctx,
              'Price',
              '\$${p.value.toStringAsFixed(2)}',
              Slider(
                  min: 1,
                  max: 100,
                  value: p.value,
                  onChanged: (v) =>
                      p.value = double.parse(v.toStringAsFixed(2)))),
          _row(
              ctx,
              'Qty',
              '${q.value}',
              Slider(
                  min: 1,
                  max: 20,
                  divisions: 19,
                  value: q.value.toDouble(),
                  onChanged: (v) => q.value = v.round())),
          const Divider(height: 32),
          Text('Total: \$${t.value.toStringAsFixed(2)}',
              style: tt.headlineMedium?.copyWith(
                  fontWeight: FontWeight.bold, color: ctx.colorScheme.primary)),
        ]),
      ),
    );
  }

  Widget _row(BuildContext c, String label, String val, Widget slider) => Row(
        children: [
          SizedBox(
              width: 60, child: Text(label, style: c.textTheme.titleMedium)),
          Expanded(child: slider),
          SizedBox(width: 70, child: Text(val, textAlign: TextAlign.end)),
        ],
      );
}

// -- 3. Async: loading / error / data ----------------------------------------
Future<String> _fetchUser() async {
  await Future.delayed(const Duration(seconds: 2));
  if (Random().nextBool()) {
    final n = DateTime.now();
    return 'Jane Doe (${n.hour}:${n.minute.toString().padLeft(2, '0')})';
  }
  throw Exception('Network error - tap Retry');
}

class _AsyncTab extends StatelessWidget {
  const _AsyncTab({required this.notifier});
  final ReactiveAsyncNotifier<String> notifier;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ReactiveAsync<String>(
        notifier: notifier,
        autoDispose: false,
        initial: (_) => Column(mainAxisSize: MainAxisSize.min, children: [
          const Icon(Icons.person_outline, size: 64),
          const SizedBox(height: 16),
          FilledButton(
            onPressed: () => notifier.execute(_fetchUser),
            child: const Text('Fetch User'),
          ),
        ]),
        loading: (_) => const Column(mainAxisSize: MainAxisSize.min, children: [
          CircularProgressIndicator(),
          SizedBox(height: 16),
          Text('Loading user...'),
        ]),
        error: (ctx, error, _) =>
            Column(mainAxisSize: MainAxisSize.min, children: [
          Icon(Icons.error_outline, size: 64, color: ctx.colorScheme.error),
          const SizedBox(height: 8),
          Text('$error', style: TextStyle(color: ctx.colorScheme.error)),
          const SizedBox(height: 16),
          Row(mainAxisSize: MainAxisSize.min, children: [
            FilledButton.icon(
              onPressed: () => notifier.execute(_fetchUser, retryCount: 2),
              icon: const Icon(Icons.refresh),
              label: const Text('Retry (up to 2x)'),
            ),
            const SizedBox(width: 12),
            OutlinedButton(
                onPressed: notifier.reset, child: const Text('Reset')),
          ]),
        ]),
        data: (ctx, user) => Column(mainAxisSize: MainAxisSize.min, children: [
          const CircleAvatar(radius: 40, child: Icon(Icons.person, size: 48)),
          const SizedBox(height: 16),
          Text(user, style: ctx.textTheme.titleLarge),
          const SizedBox(height: 16),
          OutlinedButton(
            onPressed: () => notifier.execute(_fetchUser),
            child: const Text('Fetch Again'),
          ),
        ]),
      ),
    );
  }
}

// -- 4. Undo / Redo ----------------------------------------------------------
class _UndoTab extends StatelessWidget {
  const _UndoTab({required this.notifier});
  final UndoableNotifier<int> notifier;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Reactive<int>(
        notifier: notifier,
        autoDispose: false,
        builder: (ctx, value, _) => Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$value', style: ctx.textTheme.displayLarge),
            const SizedBox(height: 24),
            Wrap(spacing: 8, children: [
              for (final n in [1, 5, 10])
                ActionChip(
                    label: Text('+$n'),
                    onPressed: () => notifier.value = value + n),
            ]),
            const SizedBox(height: 24),
            Row(mainAxisSize: MainAxisSize.min, children: [
              IconButton.filled(
                onPressed: notifier.canUndo ? notifier.undo : null,
                icon: const Icon(Icons.undo),
                tooltip: 'Undo',
              ),
              const SizedBox(width: 16),
              IconButton.filled(
                onPressed: notifier.canRedo ? notifier.redo : null,
                icon: const Icon(Icons.redo),
                tooltip: 'Redo',
              ),
            ]),
            const SizedBox(height: 8),
            Text(
              'History: ${notifier.canUndo ? "yes" : "empty"} | '
              'Future: ${notifier.canRedo ? "yes" : "empty"}',
              style: ctx.textTheme.bodySmall,
            ),
          ],
        ),
      ),
    );
  }
}

// -- 5. Search: ReactiveDebouncedNotifier --------------------------------------------
const _kItems = [
  'Flutter',
  'Dart',
  'React',
  'Angular',
  'Vue',
  'Svelte',
  'SwiftUI',
  'Kotlin',
  'Compose',
  'Jetpack',
  'Riverpod',
  'Provider',
  'Bloc',
  'OptiReact',
  'Redux',
  'MobX',
];

class _SearchTab extends StatefulWidget {
  const _SearchTab({required this.search, required this.results});
  final ReactiveDebouncedNotifier<String> search;
  final ReactiveList<String> results;
  @override
  State<_SearchTab> createState() => _SearchTabState();
}

class _SearchTabState extends State<_SearchTab> {
  void _onSearchChanged() {
    final q = widget.search.value.toLowerCase();
    final matches = q.isEmpty
        ? <String>[]
        : _kItems.where((i) => i.toLowerCase().contains(q)).toList();
    widget.results.clear();
    if (matches.isNotEmpty) widget.results.addAll(matches);
  }

  @override
  void initState() {
    super.initState();
    widget.search.addListener(_onSearchChanged);
  }

  @override
  void dispose() {
    widget.search.removeListener(_onSearchChanged);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(children: [
        TextField(
          decoration: const InputDecoration(
            prefixIcon: Icon(Icons.search),
            hintText: 'Type to search (400 ms debounce)...',
            border: OutlineInputBorder(),
          ),
          onChanged: (v) => widget.search.value = v,
        ),
        const SizedBox(height: 16),
        Expanded(
          child: Reactive<List<String>>(
            notifier: widget.results,
            autoDispose: false,
            builder: (ctx, items, _) {
              if (items.isEmpty) {
                return Center(
                    child: Text(
                  widget.search.value.isEmpty
                      ? 'Start typing to search'
                      : 'No results',
                  style: ctx.textTheme.bodyLarge
                      ?.copyWith(color: ctx.colorScheme.outline),
                ));
              }
              return ListView.separated(
                itemCount: items.length,
                separatorBuilder: (_, __) => const Divider(height: 1),
                itemBuilder: (_, i) => ListTile(
                  leading: CircleAvatar(child: Text(items[i][0])),
                  title: Text(items[i]),
                ),
              );
            },
          ),
        ),
      ]),
    );
  }
}

// -- Helpers -----------------------------------------------------------------
extension on BuildContext {
  TextTheme get textTheme => Theme.of(this).textTheme;
  ColorScheme get colorScheme => Theme.of(this).colorScheme;
}
2
likes
160
points
118
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A lightweight, high-performance reactive state management library for Flutter. Zero external dependencies, auto-dispose, async handling, undo/redo, rate limiting, and more.

Repository (GitHub)
View/report issues

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter

More

Packages that depend on optireact