flutter_debounce_throttle 2.4.5 copy "flutter_debounce_throttle: ^2.4.5" to clipboard
flutter_debounce_throttle: ^2.4.5 copied to clipboard

Better than easy_debounce. Enterprise debounce/throttle with Redis rate limiting, gesture throttling, 360+ tests. Memory-safe, actively maintained.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_debounce_throttle Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
      home: const DemoHome(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('flutter_debounce_throttle'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.touch_app), text: 'Anti-Spam'),
              Tab(icon: Icon(Icons.search), text: 'Search'),
              Tab(icon: Icon(Icons.upload_rounded), text: 'Async Form'),
              Tab(icon: Icon(Icons.layers), text: 'Concurrency'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            _AntiSpamTab(),
            _SearchTab(),
            _AsyncFormTab(),
            _ConcurrencyTab(),
          ],
        ),
      ),
    );
  }
}

// ─── Tab 1: Anti-Spam Button (Throttle) ───────────────────────────────────────

class _AntiSpamTab extends StatefulWidget {
  const _AntiSpamTab();

  @override
  State<_AntiSpamTab> createState() => _AntiSpamTabState();
}

class _AntiSpamTabState extends State<_AntiSpamTab> {
  int _taps = 0;
  int _executed = 0;
  late final Throttler _throttler;

  @override
  void initState() {
    super.initState();
    _throttler = Throttler(duration: 2.seconds);
  }

  @override
  void dispose() {
    _throttler.dispose();
    super.dispose();
  }

  void _handleTap() {
    setState(() => _taps++);
    _throttler(() => setState(() => _executed++));
  }

  @override
  Widget build(BuildContext context) {
    final blocked = _taps - _executed;
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Tap as fast as you can!',
              style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 4),
          const Text('Only 1 payment fires per 2 seconds.',
              style: TextStyle(color: Colors.grey)),
          const SizedBox(height: 36),
          GestureDetector(
            onTap: _handleTap,
            child: Container(
              padding:
                  const EdgeInsets.symmetric(horizontal: 48, vertical: 18),
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(14),
                boxShadow: [
                  BoxShadow(
                    color: Colors.blue.withAlpha(76),
                    blurRadius: 12,
                    offset: const Offset(0, 4),
                  )
                ],
              ),
              child: const Text('Pay \$99',
                  style: TextStyle(
                      color: Colors.white,
                      fontSize: 20,
                      fontWeight: FontWeight.bold)),
            ),
          ),
          const SizedBox(height: 36),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              _StatCard(label: 'Taps', value: '$_taps', color: Colors.orange),
              _StatCard(
                  label: 'Payments',
                  value: '$_executed',
                  color: Colors.green),
              _StatCard(
                  label: 'Blocked', value: '$blocked', color: Colors.red),
            ],
          ),
          const SizedBox(height: 32),
          _CodeSnippet(
              'Throttler(duration: 2.seconds)\n'
              '// 1 execution per 2 seconds — rest are dropped'),
        ],
      ),
    );
  }
}

// ─── Tab 2: Debounced Search ───────────────────────────────────────────────────

class _SearchTab extends StatefulWidget {
  const _SearchTab();

  @override
  State<_SearchTab> createState() => _SearchTabState();
}

class _SearchTabState extends State<_SearchTab> {
  List<String> _results = [];
  int _totalKeystrokes = 0;
  int _apiCalls = 0;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          const SizedBox(height: 8),
          DebouncedQueryBuilder<List<String>>(
            duration: 500.ms,
            onQuery: (text) async {
              setState(() => _apiCalls++);
              await Future.delayed(const Duration(milliseconds: 600));
              return List.generate(
                  4, (i) => '${text.isNotEmpty ? text : "flutter"} result ${i + 1}');
            },
            onResult: (results) {
              if (mounted) setState(() => _results = results);
            },
            builder: (context, search, isSearching) => TextField(
              onChanged: (text) {
                setState(() => _totalKeystrokes++);
                search?.call(text); // search is nullable — called only after debounce
              },
              decoration: InputDecoration(
                hintText: 'Type to search...',
                border: const OutlineInputBorder(),
                prefixIcon: const Icon(Icons.search),
                suffixIcon: isSearching
                    ? const Padding(
                        padding: EdgeInsets.all(12),
                        child: SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        ),
                      )
                    : null,
              ),
            ),
          ),
          const SizedBox(height: 12),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _StatCard(
                  label: 'Keystrokes',
                  value: '$_totalKeystrokes',
                  color: Colors.orange),
              _StatCard(
                  label: 'API Calls', value: '$_apiCalls', color: Colors.blue),
              _StatCard(
                  label: 'Saved',
                  value: '${(_totalKeystrokes - _apiCalls).clamp(0, 9999)}',
                  color: Colors.green),
            ],
          ),
          const SizedBox(height: 12),
          Expanded(
            child: _results.isEmpty
                ? const Center(
                    child: Text('Start typing to search',
                        style: TextStyle(color: Colors.grey)))
                : ListView.builder(
                    itemCount: _results.length,
                    itemBuilder: (_, i) => ListTile(
                      leading: const Icon(Icons.article_outlined),
                      title: Text(_results[i]),
                    ),
                  ),
          ),
          _CodeSnippet(
              'DebouncedQueryBuilder(\n'
              '  duration: 500.ms,\n'
              '  onQuery: (text) async => await api.search(text),\n'
              '  onResult: (results) => setState(() => _results = results),\n'
              ')'),
        ],
      ),
    );
  }
}

// ─── Tab 3: Async Form (ConcurrentAsyncThrottledBuilder — drop mode) ──────────

class _AsyncFormTab extends StatefulWidget {
  const _AsyncFormTab();

  @override
  State<_AsyncFormTab> createState() => _AsyncFormTabState();
}

class _AsyncFormTabState extends State<_AsyncFormTab> {
  int _attempts = 0;
  int _submitted = 0;
  String _lastStatus = '';

  @override
  Widget build(BuildContext context) {
    final blocked = _attempts - _submitted;
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Async Form Submit', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 4),
          const Text('Duplicate taps are dropped while submitting.',
              style: TextStyle(color: Colors.grey)),
          const SizedBox(height: 36),
          ConcurrentAsyncThrottledBuilder(
            mode: ConcurrencyMode.drop,
            onPressed: () async {
              setState(() {
                _attempts++;
                _lastStatus = 'Submitting...';
              });
              await Future.delayed(const Duration(seconds: 2));
              if (mounted) {
                setState(() {
                  _submitted++;
                  _lastStatus = 'Submitted!';
                });
              }
            },
            builder: (context, callback, isLoading, _) => SizedBox(
              width: double.infinity,
              child: FilledButton.icon(
                onPressed: callback, // null when busy — button auto-disabled
                icon: isLoading
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(
                            color: Colors.white, strokeWidth: 2),
                      )
                    : const Icon(Icons.send),
                label: Text(isLoading ? 'Submitting...' : 'Submit Form'),
              ),
            ),
          ),
          const SizedBox(height: 16),
          if (_lastStatus.isNotEmpty)
            Text(_lastStatus,
                style: TextStyle(
                    color: _lastStatus.contains('!')
                        ? Colors.green
                        : Colors.orange)),
          const SizedBox(height: 32),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              _StatCard(
                  label: 'Attempts', value: '$_attempts', color: Colors.orange),
              _StatCard(
                  label: 'Submitted', value: '$_submitted', color: Colors.green),
              _StatCard(
                  label: 'Blocked', value: '$blocked', color: Colors.red),
            ],
          ),
          const SizedBox(height: 32),
          _CodeSnippet(
              'ConcurrentAsyncThrottledBuilder(\n'
              '  mode: ConcurrencyMode.drop,\n'
              '  onPressed: () async => await submitForm(),\n'
              '  builder: (ctx, callback, isLoading, _) =>\n'
              '    FilledButton(onPressed: callback, ...),\n'
              ')'),
        ],
      ),
    );
  }
}

// ─── Tab 4: Concurrency — Replace Mode ────────────────────────────────────────

class _ConcurrencyTab extends StatefulWidget {
  const _ConcurrencyTab();

  @override
  State<_ConcurrencyTab> createState() => _ConcurrencyTabState();
}

class _ConcurrencyTabState extends State<_ConcurrencyTab> {
  late final ConcurrentAsyncThrottler _throttler;
  final List<_RequestLog> _log = [];
  int _requestId = 0;
  bool _isRunning = false;

  @override
  void initState() {
    super.initState();
    _throttler = ConcurrentAsyncThrottler(mode: ConcurrencyMode.replace);
  }

  @override
  void dispose() {
    _throttler.dispose();
    super.dispose();
  }

  void _addLog(String message, {bool done = false}) {
    if (!mounted) return;
    setState(() {
      _log.insert(0, _RequestLog(message, done: done));
      if (_log.length > 8) _log.removeLast();
    });
  }

  Future<void> _handleSearch(String text) async {
    if (text.isEmpty) return;
    final id = ++_requestId;
    _addLog('Request #$id started: "$text"');
    setState(() => _isRunning = true);

    await _throttler(() async {
      await Future.delayed(const Duration(milliseconds: 800));
      _addLog('Request #$id completed ✓', done: true);
    });

    if (mounted) setState(() => _isRunning = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          const SizedBox(height: 8),
          Text('ConcurrencyMode.replace',
              style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 4),
          const Text(
              'Each new search cancels the previous in-flight request.\n'
              'Only the latest result is used.',
              textAlign: TextAlign.center,
              style: TextStyle(color: Colors.grey, fontSize: 13)),
          const SizedBox(height: 16),
          TextField(
            onChanged: _handleSearch,
            decoration: InputDecoration(
              hintText: 'Type quickly to see cancellation in action...',
              border: const OutlineInputBorder(),
              prefixIcon: _isRunning
                  ? const Padding(
                      padding: EdgeInsets.all(12),
                      child: SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      ),
                    )
                  : const Icon(Icons.search),
            ),
          ),
          const SizedBox(height: 12),
          Expanded(
            child: _log.isEmpty
                ? const Center(
                    child: Text('Request log will appear here',
                        style: TextStyle(color: Colors.grey)))
                : ListView.builder(
                    itemCount: _log.length,
                    itemBuilder: (_, i) {
                      final entry = _log[i];
                      return ListTile(
                        dense: true,
                        leading: Icon(
                          entry.done
                              ? Icons.check_circle_outline
                              : Icons.pending_outlined,
                          color: entry.done ? Colors.green : Colors.orange,
                          size: 18,
                        ),
                        title: Text(entry.message,
                            style: const TextStyle(fontSize: 13)),
                      );
                    },
                  ),
          ),
          _CodeSnippet(
              'ConcurrentAsyncThrottler(\n'
              '  mode: ConcurrencyMode.replace,\n'
              ')\n'
              '// New search cancels the previous one automatically'),
        ],
      ),
    );
  }
}

class _RequestLog {
  final String message;
  final bool done;
  _RequestLog(this.message, {this.done = false});
}

// ─── Shared Widgets ────────────────────────────────────────────────────────────

class _StatCard extends StatelessWidget {
  final String label;
  final String value;
  final Color color;

  const _StatCard(
      {required this.label, required this.value, required this.color});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 90,
      padding: const EdgeInsets.symmetric(vertical: 12),
      decoration: BoxDecoration(
        color: color.withAlpha(25),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: color.withAlpha(76)),
      ),
      child: Column(
        children: [
          Text(value,
              style: TextStyle(
                  fontSize: 28, fontWeight: FontWeight.bold, color: color)),
          const SizedBox(height: 2),
          Text(label,
              style: const TextStyle(fontSize: 12, color: Colors.grey)),
        ],
      ),
    );
  }
}

class _CodeSnippet extends StatelessWidget {
  final String code;
  const _CodeSnippet(this.code);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.grey[100],
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.grey[300]!),
      ),
      child: Text(
        code,
        style: TextStyle(
            fontFamily: 'monospace', fontSize: 12, color: Colors.grey[800]),
      ),
    );
  }
}
5
likes
160
points
280
downloads

Publisher

verified publisherbrewkits.dev

Weekly Downloads

Better than easy_debounce. Enterprise debounce/throttle with Redis rate limiting, gesture throttling, 360+ tests. Memory-safe, actively maintained.

Repository (GitHub)
View/report issues
Contributing

Topics

#debounce #throttle #rate-limiting #ui #widget

Documentation

API reference

License

MIT (license)

Dependencies

dart_debounce_throttle, flutter, meta

More

Packages that depend on flutter_debounce_throttle