How to Eliminate Granny Clicks in Flutter

How to Eliminate Granny Clicks in Flutter

Sometimes, users spam your buttons; there are ways to avoid this.

Grandma looking at a computer.

You’ve probably encountered this scenario before: you click a button, you’re not sure if it worked, so you spam it a few times to make sure.

button_spam_fail

What’s happening behind the scenes is most likely a dumpster fire. Those requests then all go to your API, some fail, and some go through, which can mess up systems when not handled properly. This phenomenon is commonly referred to as granny clicking, spamming, or button mashing.

peter

In Flutter, we can fix this by using Mixins.

Primary Button with Loading State

This StatefulWidget is a primary button. By using the keyword with, we can add the LoadingStateMixin to our widget. This provides us with an isLoading notifier and a function called withLoading.

We will pass our callback into the withLoading function, which will trigger the loading state and block any other button taps until the loading is complete.

class PrimaryButton extends StatefulWidget {
final String text;
final Future<void> Function() onPressed;
const PrimaryButton({
super.key,
required this.text,
required this.onPressed,
});
@override
State<PrimaryButton> createState() => _PrimaryButtonState();
}
class _PrimaryButtonState extends State<PrimaryButton> with LoadingStateMixin {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isLoading,
builder: (context, loading, child) {
return InkWell(
onTap: withLoading(widget.onPressed),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Text(
widget.text,
),
)
);
},
);
}
}

If the withLoading method is called while it’s already in the process of loading, it will simply return early. Otherwise, it will call the callback function once it’s done running and toggle the loading state on the notifier.

mixin LoadingStateMixin<T extends StatefulWidget> on State<T> {
final ValueNotifier<bool> isLoading = ValueNotifier(false);
Future<void> withLoading(Future<void> Function() callback) async {
if (isLoading.value) {
return;
}
isLoading.value = true;
try {
await callback();
} finally {
isLoading.value = false;
}
}
@override
void dispose() {
isLoading.dispose();
super.dispose();
}
}

In our button scenario, you would want to show a loading indication when there are asynchronous tasks running. You have access to the loading state, so you can change the UI however you prefer. The nice aspect of using this mixin is that the logic and granny taps will never occur while it is in use. A fancier UI, while recommended, is entirely optional.

Course thumbnail

Want to learn Flutter?

We teach you all you need to confidently build apps at hungrimind.com/learn/flutter

Using a Mixin

The main reason we use a mixin for this is to avoid reimplementing the functionality of both loading indication and spam tapping on buttons for every button. All this is accomplished with a single mixin.

button_mixin_success

Thank you for reading!

YouTube Video

Get articles right in your inbox

No spam, unsubscribe anytime. We treat your inbox with respect.