async_value_notifier 1.1.0 copy "async_value_notifier: ^1.1.0" to clipboard
async_value_notifier: ^1.1.0 copied to clipboard

An asynchronous variant of ValueNotifier that coalesces multiple value assignments within the same event‑loop turn into a single notification dispatched in a later microtask.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:async_value_notifier/async_value_notifier.dart';

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

enum TaskStatus { ready, running, ended }

class LogBuffer {
  final _logs = <String>[];
  final logsNotifier = ValueNotifier<int>(0); // trigger UI rebuild

  List<String> get logs => List.unmodifiable(_logs);

  void add(String line) {
    // Use only the time part for brevity
    final ts = DateTime.now().toIso8601String().split('T').last;
    _logs.add('[$ts] $line');
    if (_logs.length > 200) _logs.removeRange(0, _logs.length - 200);
    logsNotifier.value++;
    // Also print to console
    // ignore: avoid_print
    print(line);
  }

  void clear() {
    _logs.clear();
    logsNotifier.value++;
  }
}

final log = LogBuffer();

/// Using a normal ValueNotifier: listeners fire synchronously.
/// This allows code inserted by listeners to override state changes that happen later in the same sequential logic.
class TaskControllerSync {
  final status = ValueNotifier(TaskStatus.ready);
  final errorCode = ValueNotifier<int?>(null);

  TaskControllerSync() {
    errorCode.addListener(() {
      if (errorCode.value != null) {
        log.add('[sync] error listener: set status -> ready (to allow retry)');
        status.value = TaskStatus.ready; // will be overwriten
      }
    });
  }

  Future<void> startTask() async {
    if (status.value != TaskStatus.ready) {
      log.add('[sync] task is not ready');
      return;
    }
    errorCode.value = null;
    log.add('[sync] start task');
    status.value = TaskStatus.running;
    try {
      await Future.delayed(const Duration(milliseconds: 300));
      throw Exception('network error');
    } catch (e) {
      log.add('[sync] set errorCode');
      errorCode.value = 500; // triggers listener immediately
    }
    log.add('[sync] set status -> ended');
    status.value = TaskStatus.ended; // overwrites listener ready
  }
}

/// Using AsyncValueNotifier: listener notifications are batched in a microtask.
/// Sequential logic completes first; listeners run afterward, avoiding mid-flow interference.
class TaskControllerAsync {
  final status = AsyncValueNotifier(
    TaskStatus.ready,
    weakListener: true,
    cancelable: true,
    distinct: true,
  );
  final errorCode = AsyncValueNotifier<int?>(null);

  TaskControllerAsync() {
    errorCode.addListener(() {
      if (errorCode.value != null) {
        log.add('[async] error listener: set status -> ready (to allow retry)');
        status.value = TaskStatus.ready; // Final state will be ready
      }
    });
  }

  Future<void> startTask() async {
    if (status.value != TaskStatus.ready) {
      log.add('[async] task is not ready');
      return;
    }
    errorCode.value = null;
    log.add('[async] start task');
    status.value = TaskStatus.running;
    try {
      await Future.delayed(const Duration(milliseconds: 300));
      throw Exception('network error');
    } catch (e) {
      log.add('[async] set errorCode');
      errorCode.value = 500; // listener not triggered yet
    }
    log.add('[async] set status -> ended');
    status.value = TaskStatus.ended; // before notifying listeners
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AsyncValueNotifier Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final TaskControllerSync syncCtrl;
  late final TaskControllerAsync asyncCtrl;

  @override
  void initState() {
    super.initState();
    syncCtrl = TaskControllerSync();
    asyncCtrl = TaskControllerAsync();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AsyncValueNotifier vs ValueNotifier')),
      body: Column(
        children: [
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Demo: task fails -> set error code -> set status to ended. Error listener tries to reset status to ready for retry.',
                  ),
                  const SizedBox(height: 12),
                  _SectionTitle('1. Synchronous ValueNotifier (problematic)'),
                  _SyncStatusView(ctrl: syncCtrl),
                  const SizedBox(height: 8),
                  _SectionTitle('2. AsyncValueNotifier (works as intended)'),
                  _AsyncStatusView(ctrl: asyncCtrl),
                  const Divider(height: 32),
                  Row(
                    children: [
                      ElevatedButton(
                        onPressed: () {
                          log.clear();
                          syncCtrl.startTask();
                        },
                        child: const Text('Run sync task'),
                      ),
                      const SizedBox(width: 12),
                      ElevatedButton(
                        onPressed: () {
                          log.clear();
                          asyncCtrl.startTask();
                        },
                        child: const Text('Run async task'),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16),
                  const Text('Logs (newest first):'),
                  ValueListenableBuilder(
                    valueListenable: log.logsNotifier,
                    builder: (_, __, ___) {
                      final lines = log.logs.reversed.toList();
                      return Container(
                        width: double.infinity,
                        padding: const EdgeInsets.all(8),
                        decoration: BoxDecoration(
                          color: Colors.black87,
                          borderRadius: BorderRadius.circular(6),
                        ),
                        child: SelectableText(
                          lines.join('\n'),
                          style: const TextStyle(
                            fontFamily: 'monospace',
                            fontSize: 12,
                            color: Colors.greenAccent,
                          ),
                        ),
                      );
                    },
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _SyncStatusView extends StatelessWidget {
  final TaskControllerSync ctrl;
  const _SyncStatusView({required this.ctrl});

  String _statusText(TaskStatus s) => s.name;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        ValueListenableBuilder(
          valueListenable: ctrl.status,
          builder: (_, s, __) => Chip(
            label: Text('status: ${_statusText(s)}'),
            backgroundColor: Colors.red.withValues(alpha: 0.1),
          ),
        ),
        const SizedBox(width: 8),
        ValueListenableBuilder(
          valueListenable: ctrl.errorCode,
          builder: (_, e, __) => Chip(
            label: Text('error: ${e ?? '-'}'),
            backgroundColor: Colors.orange.withValues(alpha: 0.15),
          ),
        ),
      ],
    );
  }
}

class _AsyncStatusView extends StatelessWidget {
  final TaskControllerAsync ctrl;
  const _AsyncStatusView({required this.ctrl});

  String _statusText(TaskStatus s) => s.name;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        ValueListenableBuilder<TaskStatus>(
          valueListenable: ctrl.status,
          builder: (_, s, __) => Chip(
            label: Text('status: ${_statusText(s)}'),
            backgroundColor: Colors.green.withValues(alpha: 0.15),
          ),
        ),
        const SizedBox(width: 8),
        ValueListenableBuilder<int?>(
          valueListenable: ctrl.errorCode,
          builder: (_, e, __) => Chip(
            label: Text('error: ${e ?? '-'}'),
            backgroundColor: Colors.blue.withValues(alpha: 0.15),
          ),
        ),
      ],
    );
  }
}

class _SectionTitle extends StatelessWidget {
  final String text;
  const _SectionTitle(this.text);
  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 4),
        child: Text(text, style: Theme.of(context).textTheme.titleMedium),
      );
}
0
likes
160
points
158
downloads

Publisher

unverified uploader

Weekly Downloads

An asynchronous variant of ValueNotifier that coalesces multiple value assignments within the same event‑loop turn into a single notification dispatched in a later microtask.

Repository (GitHub)
View/report issues

Topics

#async #notifier #batching #tweak #performance

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on async_value_notifier