flutter_event_limiter 1.0.3 copy "flutter_event_limiter: ^1.0.3" to clipboard
flutter_event_limiter: ^1.0.3 copied to clipboard

Throttle and debounce for Flutter. Prevent double-clicks, race conditions, memory leaks. Universal Builders for ANY widget with automatic loading states.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Event Limiter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const DemoHomePage(),
    );
  }
}

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

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage> {
  int _currentIndex = 0;

  final List<Widget> _pages = const [
    ThrottleDemo(),
    DebounceDemo(),
    AsyncThrottleDemo(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Flutter Event Limiter Demo'),
      ),
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) => setState(() => _currentIndex = index),
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.touch_app),
            label: 'Throttle',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Debounce',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.upload),
            label: 'Async',
          ),
        ],
      ),
    );
  }
}

// ============================================================
// TAB 1: THROTTLE DEMO (Prevent Double Clicks)
// ============================================================

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

  @override
  State<ThrottleDemo> createState() => _ThrottleDemoState();
}

class _ThrottleDemoState extends State<ThrottleDemo> {
  int _normalClickCount = 0;
  int _throttledClickCount = 0;
  final List<String> _logs = [];

  void _addLog(String message) {
    setState(() {
      _logs.insert(
          0, '${DateTime.now().toIso8601String().substring(11, 23)}: $message');
      if (_logs.length > 10) _logs.removeLast();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            '🎯 Double-Click Test',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Try clicking both buttons rapidly to see the difference!',
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
          const SizedBox(height: 24),

          // Normal Button (No Protection)
          Card(
            color: Colors.red.shade50,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                children: [
                  const Text(
                    '❌ Normal Button (No Protection)',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 12),
                  InkWell(
                    onTap: () {
                      setState(() => _normalClickCount++);
                      _addLog('❌ Normal click #$_normalClickCount');
                    },
                    child: Container(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Center(
                        child: Text(
                          'Click Me Fast!',
                          style: TextStyle(color: Colors.white, fontSize: 16),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Clicks: $_normalClickCount',
                    style: const TextStyle(
                        fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 16),

          // Throttled Button (Protected)
          Card(
            color: Colors.green.shade50,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                children: [
                  const Text(
                    '✅ Throttled Button (Protected)',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 12),
                  ThrottledInkWell(
                    duration: const Duration(milliseconds: 500),
                    onTap: () {
                      setState(() => _throttledClickCount++);
                      _addLog('✅ Throttled click #$_throttledClickCount');
                    },
                    child: Container(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                      decoration: BoxDecoration(
                        color: Colors.green,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Center(
                        child: Text(
                          'Click Me Fast!',
                          style: TextStyle(color: Colors.white, fontSize: 16),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Clicks: $_throttledClickCount (500ms throttle)',
                    style: const TextStyle(
                        fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 24),

          // Logs
          const Text(
            'Event Log:',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.grey.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              child: _logs.isEmpty
                  ? const Center(child: Text('Click buttons to see logs'))
                  : ListView.builder(
                      itemCount: _logs.length,
                      itemBuilder: (context, index) {
                        return Padding(
                          padding: const EdgeInsets.symmetric(vertical: 2),
                          child: Text(
                            _logs[index],
                            style: const TextStyle(
                                fontFamily: 'monospace', fontSize: 12),
                          ),
                        );
                      },
                    ),
            ),
          ),
        ],
      ),
    );
  }
}

// ============================================================
// TAB 2: DEBOUNCE DEMO (Search API)
// ============================================================

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

  @override
  State<DebounceDemo> createState() => _DebounceDemoState();
}

class _DebounceDemoState extends State<DebounceDemo> {
  final List<String> _results = [];
  bool _isLoading = false;
  final List<String> _logs = [];

  late final AsyncDebouncedTextController<List<String>> _controller;

  @override
  void initState() {
    super.initState();
    _controller = AsyncDebouncedTextController<List<String>>(
      duration: const Duration(milliseconds: 300),
      onChanged: (text) async {
        _addLog('🔍 Searching for "$text"...');
        // Simulate API call
        await Future.delayed(const Duration(milliseconds: 500));
        _addLog('✅ Got results for "$text"');
        return _mockSearch(text);
      },
      onSuccess: (results) {
        setState(() => _results
          ..clear()
          ..addAll(results));
      },
      onError: (error, stack) {
        _addLog('❌ Error: $error');
      },
      onLoadingChanged: (isLoading) {
        if (mounted) {
          setState(() => _isLoading = isLoading);
        }
      },
    );
  }

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

  void _addLog(String message) {
    setState(() {
      _logs.insert(
          0, '${DateTime.now().toIso8601String().substring(11, 23)}: $message');
      if (_logs.length > 15) _logs.removeLast();
    });
  }

  List<String> _mockSearch(String query) {
    if (query.isEmpty) return [];
    final mockData = [
      'Apple',
      'Banana',
      'Cherry',
      'Date',
      'Elderberry',
      'Fig',
      'Grape',
      'Honeydew',
      'Kiwi',
      'Lemon',
      'Mango',
      'Orange',
      'Papaya',
      'Raspberry',
      'Strawberry',
    ];
    return mockData
        .where((item) => item.toLowerCase().contains(query.toLowerCase()))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            '🔍 Search API Demo',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Type quickly and watch debouncing + auto-cancel in action!',
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
          const SizedBox(height: 24),

          // Search Field
          TextField(
            controller: _controller.textController,
            decoration: InputDecoration(
              labelText: 'Search Fruits',
              hintText: 'Try typing "apple" quickly...',
              prefixIcon: const Icon(Icons.search),
              suffixIcon: _isLoading
                  ? const Padding(
                      padding: EdgeInsets.all(12.0),
                      child: SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      ),
                    )
                  : null,
              border: const OutlineInputBorder(),
            ),
          ),

          const SizedBox(height: 16),

          // Results
          Text(
            'Results (${_results.length}):',
            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: _results.isEmpty
                ? const Center(child: Text('Start typing to search...'))
                : ListView.builder(
                    itemCount: _results.length,
                    itemBuilder: (context, index) {
                      return Card(
                        child: ListTile(
                          leading: const Icon(Icons.apple),
                          title: Text(_results[index]),
                        ),
                      );
                    },
                  ),
          ),

          const SizedBox(height: 16),

          // Logs
          const Text(
            'Event Log:',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Container(
            height: 150,
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey.shade100,
              borderRadius: BorderRadius.circular(8),
            ),
            child: _logs.isEmpty
                ? const Center(child: Text('Type to see logs'))
                : ListView.builder(
                    itemCount: _logs.length,
                    itemBuilder: (context, index) {
                      return Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          _logs[index],
                          style: const TextStyle(
                              fontFamily: 'monospace', fontSize: 12),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

// ============================================================
// TAB 3: ASYNC THROTTLE DEMO (Form Submit)
// ============================================================

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

  @override
  State<AsyncThrottleDemo> createState() => _AsyncThrottleDemoState();
}

class _AsyncThrottleDemoState extends State<AsyncThrottleDemo> {
  final List<String> _logs = [];
  int _uploadCount = 0;

  void _addLog(String message) {
    setState(() {
      _logs.insert(
          0, '${DateTime.now().toIso8601String().substring(11, 23)}: $message');
      if (_logs.length > 10) _logs.removeLast();
    });
  }

  Future<void> _simulateUpload() async {
    _addLog('📤 Starting upload...');
    await Future.delayed(const Duration(seconds: 2)); // Simulate network delay
    setState(() => _uploadCount++);
    _addLog('✅ Upload #$_uploadCount completed!');
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            '📤 Async Form Submit Demo',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Try clicking the button multiple times rapidly!',
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
          const SizedBox(height: 24),

          // Async Throttled Button
          AsyncThrottledCallbackBuilder(
            onPressed: _simulateUpload,
            onError: (error, stack) {
              _addLog('❌ Error: $error');
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('Upload failed: $error'),
                  backgroundColor: Colors.red,
                ),
              );
            },
            onSuccess: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text('Upload successful!'),
                  backgroundColor: Colors.green,
                ),
              );
            },
            builder: (context, callback, isLoading) {
              return ElevatedButton.icon(
                onPressed: isLoading ? null : callback,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 20),
                  backgroundColor: Colors.blue,
                  foregroundColor: Colors.white,
                ),
                icon: isLoading
                    ? const SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(
                          color: Colors.white,
                          strokeWidth: 2,
                        ),
                      )
                    : const Icon(Icons.cloud_upload),
                label: Text(
                  isLoading ? 'Uploading...' : 'Upload File (2s delay)',
                  style: const TextStyle(fontSize: 18),
                ),
              );
            },
          ),

          const SizedBox(height: 16),

          Text(
            'Successful Uploads: $_uploadCount',
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            textAlign: TextAlign.center,
          ),

          const SizedBox(height: 24),

          // Info Box
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.blue.shade50,
              borderRadius: BorderRadius.circular(8),
              border: Border.all(color: Colors.blue.shade200),
            ),
            child: const Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '💡 How it works:',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 8),
                Text('✅ Button locks during async operation'),
                Text('✅ Multiple clicks are ignored'),
                Text('✅ Auto-unlocks after completion'),
                Text('✅ Built-in error handling'),
                Text('✅ Loading state managed automatically'),
              ],
            ),
          ),

          const SizedBox(height: 24),

          // Logs
          const Text(
            'Event Log:',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.grey.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              child: _logs.isEmpty
                  ? const Center(child: Text('Click button to see logs'))
                  : ListView.builder(
                      itemCount: _logs.length,
                      itemBuilder: (context, index) {
                        return Padding(
                          padding: const EdgeInsets.symmetric(vertical: 2),
                          child: Text(
                            _logs[index],
                            style: const TextStyle(
                                fontFamily: 'monospace', fontSize: 12),
                          ),
                        );
                      },
                    ),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
150
downloads

Publisher

unverified uploader

Weekly Downloads

Throttle and debounce for Flutter. Prevent double-clicks, race conditions, memory leaks. Universal Builders for ANY widget with automatic loading states.

Repository (GitHub)
View/report issues

Topics

#throttle #debounce #button #widget #double-click

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_event_limiter