tap_debouncer 0.0.8

  • Readme
  • Changelog
  • Example
  • Installing
  • 67

Description #

Tap debounce simplifying widget. Wrap your button widget in TapDebounce widget and any taps will be disabled while tap callback is in progress.

Instruction #

Assume your code with some button look like this:

//...
child: RaisedButton(
  color: Colors.blue,
  disabledColor: Colors.grey,
  onPressed: () async => await someLongOperation(), // your tap handler
  child: const Text('Short'),
  );
//...

and you do not want user to be able to press the button again several times and start other someLongOperation functions. Example is a Navigator pop function - it can take a few hundred of millis to navigate and user can press the button several times, and that will lead to undesired pop several screens back instead of one.

Wrap this code to Debouncer and move RaisedButton onPressed contents to Debouncer onTap:

//...
child: TapDebouncer(
  onTap: () async => await someLongOperation(), // your tap handler moved here
  builder: (BuildContext context, TapDebouncerFunc onTap) {
    return RaisedButton(
      color: Colors.blue,
      disabledColor: Colors.grey,
      onPressed: onTap,  // It is just onTap from builder callback
      child: const Text('Short'),
    );
  },
),
//...

Debouncer will disable the RaisedButton by setting onPressed to null while onTap is being executed.

You can add optional delay to be sure that the button is disabled some time after someOperation is called.

//...
onTap: () async {
    await someOperation();
    
    await Future<void>.delayed(const Duration(milliseconds: 1000));
},
//...

You can fill optional cooldown field with some Duration and avoid adding of Future.delayed at the end of onTap callback, this will be done automatically:

//...
child: TapDebouncer(
  cooldown: const Duration(milliseconds: 1000),
  onTap: () async => await someLongOperation(), // your tap handler moved here
  builder: (BuildContext context, TapDebouncerFunc onTap) {
    return RaisedButton(
      color: Colors.blue,
      disabledColor: Colors.grey,
      onPressed: onTap,  // It is just onTap from builder callback
      child: const Text('Short'),
    );
  },
),
//...

Then your onTap could be changed to this:

//...
onTap: () async => await someOperation(),
//...

If someOperation will raise exception cooldown delay will also work, after exception.

See example application for details:

Example of button disabled after tap

0.0.8 #

  • Fix exception if onTap function is null

0.0.7 #

  • Add cooldown
  • Remove rxdart dependency
  • Update example
  • Update readme

0.0.6 #

  • Fix readme after some renamings

0.0.5 #

  • Fix readme image address

0.0.4 #

  • Fix license

0.0.3 #

  • First public release

example/lib/main.dart

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

const int kCooldownLong_ms = 3000;
const int kCooldownShort_ms = 1200;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  double _cooldown = 0;
  int _cooldownStarted = DateTime.now().millisecondsSinceEpoch;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Stack(
        children: <Widget>[
          Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'Tap detected by debounced button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.display1,
                ),
                const SizedBox(height: 24),
                const Text(
                  'Cooldown:',
                ),
                Padding(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
                  child: LinearProgressIndicator(value: _cooldown),
                ),
              ],
            ),
          ),
          Positioned(
            bottom: 20,
            right: 20,
            child: TapDebouncer(
              cooldown: const Duration(milliseconds: kCooldownShort_ms),
              onTap: () async {
                _startCooldownIndicator(kCooldownShort_ms);

                _incrementCounter();
              },
              builder: (BuildContext context, TapDebouncerFunc onTap) {
                return RaisedButton(
                  color: Colors.blue,
                  disabledColor: Colors.grey,
                  onPressed: onTap,
                  child: const Text('Short'),
                );
              },
            ),
          ),
          Positioned(
            bottom: 20,
            left: 20,
            child: TapDebouncer(
              onTap: () async {
                _startCooldownIndicator(kCooldownLong_ms);

                _incrementCounter();

                await Future<void>.delayed(
                  const Duration(milliseconds: kCooldownLong_ms),
                );
              },
              builder: (BuildContext context, TapDebouncerFunc onTap) {
                return RaisedButton(
                  color: Colors.green,
                  disabledColor: Colors.grey,
                  onPressed: onTap,
                  child: const Text('Long'),
                );
              },
            ),
          ),
          Positioned(
            top: 20,
            left: 20,
            child: TapDebouncer(
              onTap: () async {
                _incrementCounter();

                await Future<void>.delayed(TapDebouncer.kNeverCooldown);
              },
              builder: (BuildContext context, TapDebouncerFunc onTap) {
                return RaisedButton(
                  color: Colors.pink,
                  disabledColor: Colors.grey,
                  onPressed: onTap,
                  child: const Text('OneShot'),
                );
              },
            ),
          ),
          Positioned(
            top: 20,
            right: 20,
            child: Builder(
              builder: (BuildContext context) {
                return TapDebouncer(
                  cooldown: const Duration(milliseconds: kCooldownShort_ms),
                  onTap: () async {
                    _startCooldownIndicator(kCooldownShort_ms * 2);

                    await Future<void>.delayed(
                      const Duration(milliseconds: kCooldownShort_ms),
                    );

                    try {
                      throw Exception('Some error');
                    } on Exception catch (error) {
                      Scaffold.of(context).showSnackBar(SnackBar(
                        backgroundColor: Colors.red.withAlpha(0x80),
                        content: Text('Caught $error'),
                        duration: const Duration(milliseconds: 500),
                      ));
                    }
                  },
                  builder: (BuildContext context, TapDebouncerFunc onTap) {
                    return RaisedButton(
                      color: Colors.red,
                      disabledColor: Colors.grey,
                      onPressed: onTap,
                      child: const Text('Faulty'),
                    );
                  },
                );
              },
            ),
          ),
          Positioned(
            bottom: 20,
            left: 80,
            right: 80,
            child: Center(
              child: TapDebouncer(
                onTap: null,
                builder: (BuildContext context, TapDebouncerFunc onTap) {
                  return RaisedButton(
                    color: Colors.black26,
                    disabledColor: Colors.black12,
                    onPressed: onTap,
                    child: const Text('Null'),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _startCooldownIndicator(int time_ms) {
    _cooldownStarted = DateTime.now().millisecondsSinceEpoch;
    _updateCooldown(time_ms);
  }

  void _updateCooldown(int time_ms) {
    final int current = DateTime.now().millisecondsSinceEpoch;
    int delta = current - _cooldownStarted;
    if (delta > time_ms) {
      delta = time_ms;
    }

    setState(() {
      _cooldown = delta.roundToDouble() / time_ms;
    });

    Future<void>(() {
      if (delta < time_ms) {
        _updateCooldown(time_ms);
      } else {
        setState(() {
          _cooldown = 0.0;
        });
      }
    });
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  tap_debouncer: ^0.0.8

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:tap_debouncer/tap_debouncer.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
37
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
67
Learn more about scoring.

We analyzed this package on Jul 11, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.14
  • Flutter: 1.17.5

Analysis suggestions

Package not compatible with SDK dart

Because:

  • tap_debouncer that is a package requiring null.

Maintenance suggestions

Package is pre-v0.1 release. (-10 points)

While nothing is inherently wrong with versions of 0.0.*, it might mean that the author is still experimenting with the general direction of the API.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.12 1.14.13
meta 1.1.8 1.2.2
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test
pedantic ^1.8.0+1