flutter_reactive 0.1.0 copy "flutter_reactive: ^0.1.0" to clipboard
flutter_reactive: ^0.1.0 copied to clipboard

A lightweight reactive system for Flutter, inspired by simple state binding.

example/lib/main.dart

import 'dart:math';

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

import 'page2.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Reactive Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const ShowcasePage(),
    );
  }
}

class ShowcasePage extends StatefulWidget {
  const ShowcasePage({super.key});

  @override
  State<ShowcasePage> createState() => _ShowcasePageState();
}

class _ShowcasePageState extends State<ShowcasePage> {
  final Random _random = Random();
  ReactiveTransaction? _pendingTransaction;

  late final Reactive<int> txCounter = 0.reactive().require(
    (v) => v >= 0,
    'Counter cannot be negative',
  );

  late final Reactive<List<int>> numbers = <int>[8, 3, 11, 2].reactive(false);
  late final Reactive<int> strictValue = 10.reactive();
  late final Reactive<int> nonStrictValue = 10.reactive(false);
  late final Reactive<int> strictNotifications = 0.reactive(false);
  late final Reactive<int> nonStrictNotifications = 0.reactive(false);
  late final Reactive<List<String>> activityLog = <String>[].reactive(false);

  late final Reactive<String> status =
      Reactive.combine2<int, List<int>, String>(
        txCounter,
        numbers,
        (counter, items) => 'Counter: $counter | Numbers: ${items.join(", ")}',
      );

  late final Reactive<String> strictStatus =
      Reactive.combine4<int, int, int, int, String>(
        strictValue,
        nonStrictValue,
        strictNotifications,
        nonStrictNotifications,
        (strict, nonStrict, strictHits, nonStrictHits) =>
            'strict=$strict ($strictHits notifications) • '
            'nonStrict=$nonStrict ($nonStrictHits notifications)',
      );

  late final void Function(int value) _strictListener;
  late final void Function(int value) _nonStrictListener;

  @override
  void initState() {
    super.initState();

    _strictListener = (_) => strictNotifications.increment();
    _nonStrictListener = (_) => nonStrictNotifications.increment();

    strictValue.listen(_strictListener);
    nonStrictValue.listen(_nonStrictListener);

    txCounter.when((v) => v != 0 && v % 5 == 0, (v) {
      _addLog('when() fired: txCounter reached $v');
    });

    _addLog('Showcase ready. Try each section to test v0.1.0 features.');
  }

  @override
  void dispose() {
    strictValue.unlisten(_strictListener);
    nonStrictValue.unlisten(_nonStrictListener);

    txCounter.dispose();
    numbers.dispose();
    strictValue.dispose();
    nonStrictValue.dispose();
    strictNotifications.dispose();
    nonStrictNotifications.dispose();
    activityLog.dispose();
    status.dispose();
    strictStatus.dispose();
    super.dispose();
  }

  String _formatError(Object error) {
    if (error is ReactiveValidatorError) {
      return error.message;
    }
    return error.toString();
  }

  void _addLog(String message) {
    final now = DateTime.now();
    final time =
        '${now.hour.toString().padLeft(2, '0')}:'
        '${now.minute.toString().padLeft(2, '0')}:'
        '${now.second.toString().padLeft(2, '0')}';

    activityLog.mutate((logs) {
      logs.add('[$time] $message');
      if (logs.length > 30) {
        logs.removeRange(0, logs.length - 30);
      }
    });
  }

  Future<void> _runSuccessfulTransaction() async {
    final before = txCounter.value;
    _pendingTransaction = null;

    await Reactive.run(
      () {
        txCounter.inc(4);
        txCounter.dec(1);
      },
      onError: (error) {
        _addLog('Unexpected transaction error: ${_formatError(error)}');
      },
    );

    _addLog('Transaction success: $before -> ${txCounter.value}');
    if (mounted) setState(() {});
  }

  Future<void> _runAutoRollbackTransaction() async {
    final before = txCounter.value;
    _pendingTransaction = null;

    await Reactive.run(
      () {
        txCounter.inc(3);
        txCounter.dec(txCounter.value + 10); // force an invalid value (< 0)
      },
      onError: (error) {
        _addLog('Auto rollback: ${_formatError(error)}');
      },
    );

    _addLog('Auto rollback result: $before -> ${txCounter.value}');
    if (mounted) setState(() {});
  }

  Future<void> _runManualRollbackTransaction() async {
    final before = txCounter.value;

    _pendingTransaction = await Reactive.run(
      () {
        txCounter.inc(3);
        txCounter.dec(txCounter.value + 10); // force an invalid value (< 0)
      },
      rollbackOnError: false,
      onError: (error) {
        _addLog('No auto rollback: ${_formatError(error)}');
      },
    );

    _addLog(
      'Manual rollback pending: $before -> ${txCounter.value}. Tap "Rollback pending".',
    );
    if (mounted) setState(() {});
  }

  void _rollbackPendingTransaction() {
    final transaction = _pendingTransaction;
    if (transaction == null) {
      _addLog('No pending transaction to rollback.');
      return;
    }

    final before = txCounter.value;
    transaction.rollback();
    _pendingTransaction = null;
    _addLog('Manual rollback executed: $before -> ${txCounter.value}');
    updateState();
  }

  void _incrementStrictDemoValues() {
    strictValue.inc();
    nonStrictValue.inc();
    _addLog('Incremented strict and non-strict values.');
  }

  void _setSameValueOnBoth() {
    strictValue.value = strictValue.value;
    nonStrictValue.value = nonStrictValue.value;
    _addLog('Set same value on both (only non-strict notifies).');
  }

  void _resetStrictDemo() {
    strictValue.value = 10;
    nonStrictValue.value = 10;
    strictNotifications.value = 0;
    nonStrictNotifications.value = 0;
    _addLog('Strict mode demo reset.');
  }

  void _addRandomNumber() {
    final value = _random.nextInt(90) + 10;
    numbers.add(value);
    _addLog('Added $value to list.');
  }

  void _shuffleNumbers() {
    numbers.mutate((list) => list.shuffle(_random));
    _addLog('List shuffled.');
  }

  void _sortAscending() {
    numbers.sort();
    _addLog('List sorted ascending with ReactiveList.sort().');
  }

  void _sortDescending() {
    numbers.sort((a, b) => b.compareTo(a));
    _addLog('List sorted descending with custom compare.');
  }

  void _stepCounter() {
    txCounter.inc();
    _addLog('Manual increment: txCounter is now ${txCounter.value}.');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Reactive v0.1.0 Showcase'),
        actions: [
          IconButton(
            tooltip: 'Open stream page',
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder:
                      (_) => StreamShowcasePage(
                        transactionCounter: txCounter,
                        status: status,
                        activityLog: activityLog,
                      ),
                ),
              );
            },
            icon: const Icon(Icons.wifi_tethering),
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'New features in action',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 6),
            Text(
              'Transactions, when(), strict mode behavior, and list sorting.',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: ReactiveBuilder<String>(
                  reactive: status,
                  builder: (value) => Text(value),
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Transactions + rollback + validator rename',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    ReactiveBuilder<int>(
                      reactive: txCounter,
                      builder: (value) => Text('txCounter: $value'),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      _pendingTransaction == null
                          ? 'Pending rollback: none'
                          : 'Pending rollback: available',
                    ),
                    const SizedBox(height: 10),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        OutlinedButton(
                          onPressed: _stepCounter,
                          child: const Text('Counter +1 (when test)'),
                        ),
                        FilledButton(
                          onPressed: _runSuccessfulTransaction,
                          child: const Text('Run success transaction'),
                        ),
                        FilledButton.tonal(
                          onPressed: _runAutoRollbackTransaction,
                          child: const Text('Fail + auto rollback'),
                        ),
                        FilledButton.tonal(
                          onPressed: _runManualRollbackTransaction,
                          child: const Text('Fail + manual rollback'),
                        ),
                        OutlinedButton(
                          onPressed:
                              _pendingTransaction == null
                                  ? null
                                  : _rollbackPendingTransaction,
                          child: const Text('Rollback pending'),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Strict mode: `.reactive()` vs `.reactive(false)`',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    ReactiveBuilder<String>(
                      reactive: strictStatus,
                      builder: (value) => Text(value),
                    ),
                    const SizedBox(height: 10),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        FilledButton(
                          onPressed: _incrementStrictDemoValues,
                          child: const Text('Increment both'),
                        ),
                        FilledButton.tonal(
                          onPressed: _setSameValueOnBoth,
                          child: const Text('Set same value'),
                        ),
                        OutlinedButton(
                          onPressed: _resetStrictDemo,
                          child: const Text('Reset demo'),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'ReactiveList.sort()',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    ReactiveBuilder<List<int>>(
                      reactive: numbers,
                      builder: (values) => Text(values.join(' • ')),
                    ),
                    const SizedBox(height: 10),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        FilledButton(
                          onPressed: _addRandomNumber,
                          child: const Text('Add random'),
                        ),
                        FilledButton.tonal(
                          onPressed: _shuffleNumbers,
                          child: const Text('Shuffle'),
                        ),
                        FilledButton.tonal(
                          onPressed: _sortAscending,
                          child: const Text('Sort asc'),
                        ),
                        OutlinedButton(
                          onPressed: _sortDescending,
                          child: const Text('Sort desc'),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Text(
                          'Activity log',
                          style: Theme.of(context).textTheme.titleMedium,
                        ),
                        const Spacer(),
                        TextButton(
                          onPressed: activityLog.clear,
                          child: const Text('Clear'),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    SizedBox(
                      height: 220,
                      child: ReactiveBuilder<List<String>>(
                        reactive: activityLog,
                        builder: (items) {
                          if (items.isEmpty) {
                            return const Center(
                              child: Text(
                                'No events yet. Try the buttons above.',
                              ),
                            );
                          }
                          return ListView.builder(
                            reverse: true,
                            itemCount: items.length,
                            itemBuilder: (context, index) {
                              return Padding(
                                padding: const EdgeInsets.only(bottom: 6),
                                child: Text(items[index]),
                              );
                            },
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
3
likes
160
points
268
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A lightweight reactive system for Flutter, inspired by simple state binding.

Repository (GitHub)
View/report issues

License

MIT-0 (license)

Dependencies

flutter

More

Packages that depend on flutter_reactive