Multiple button presses for asynchronous click handler

699 views
Skip to first unread message

Warren Baltz

unread,
Aug 31, 2018, 1:58:30 AM8/31/18
to Flutter Dev
What's the best practice for a button that runs slow, asynchronous code?

If the button handler doesn't execute immediately there is a chance the user will press the button again and start another execution of the handler code before the first execution completes resulting in a race condition.

It seems like there should be an easy way to handle this but I haven't found one. Has anyone solved this?  (without a big home-rolled solution)


Mikkel Ravn

unread,
Aug 31, 2018, 2:03:56 AM8/31/18
to warre...@gmail.com, Flutter Dev
You can wrap the button in a FutureBuilder controlled by a future that is initialized on button click. The button's onPressed handler should be null when the future is not.

--
You received this message because you are subscribed to the Google Groups "Flutter Dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.


--
Mikkel Nygaard Ravn
Software Engineer

Warren Baltz

unread,
Aug 31, 2018, 2:34:48 AM8/31/18
to Flutter Dev
Thank you!  Would you post an example?

Mikkel Ravn

unread,
Aug 31, 2018, 3:12:57 AM8/31/18
to warre...@gmail.com, Flutter Dev
Here's an adaptation of the default Flutter counter app that "counts" to 20,000 slowly, asynchronously, and only once:

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

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(home: MyHomePage());
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
Future<int> _computation;

void _incrementCounter() {
if (_computation != null) return;
setState(() {
_computation = Future.delayed(Duration(seconds: 2)).then((_) => 20000);
});
}

@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return FutureBuilder<int>(
future: _computation,
initialData: 0,
builder: (_, snapshot) => Scaffold(
appBar: AppBar(title: Text('Count a lot')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(
'${snapshot.data}',
style: theme.textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: (_computation == null ? _incrementCounter : null),
tooltip: 'Increment',
backgroundColor:
(_computation == null ? theme.accentColor : Colors.grey),
child: Icon(Icons.add),
),
),
);
}
}

Olaide Nojeem Ekeolere

unread,
Aug 31, 2018, 4:01:18 AM8/31/18
to mr...@google.com, warre...@gmail.com, Flutter Dev
Hi,
    I have a simpler approach.
First you declare 
bool buttonPressed = false;
Remember that a button is disabled when its onPressed is null so you do this to the button
onPressed: buttonPressed? null : onPressedAction,
Then your action can now be like
onPressedAction(){
    setState(()=>buttonPressed = true);
    myAsyncMethod();
}
Then in your method
myAsyncMethod() async{
    //do whatever you want
    setState(()=>buttonPressed = false);
}
This should solve the problem, button will be disabled after click then enabled again when done.

Mikkel Ravn

unread,
Aug 31, 2018, 4:25:38 AM8/31/18
to iamjav...@gmail.com, warre...@gmail.com, Flutter Dev
On Fri, Aug 31, 2018 at 10:01 AM Olaide Nojeem Ekeolere <iamjav...@gmail.com> wrote:
Hi,
    I have a simpler approach.
First you declare 
bool buttonPressed = false;
Remember that a button is disabled when its onPressed is null so you do this to the button
onPressed: buttonPressed? null : onPressedAction,
Then your action can now be like
onPressedAction(){
    setState(()=>buttonPressed = true);
    myAsyncMethod();
}
Then in your method
myAsyncMethod() async{
    //do whatever you want
    setState(()=>buttonPressed = false);
}
This should solve the problem, button will be disabled after click then enabled again when done.

Yep. Except that
  1. The call to setState in the async method needs to be guarded with a check of isMounted.
  2. If the async method results in a value to be used by the UI, you'd need a separate state field for that and deal with transitioning through a) not set yet because button not clicked, b) not set yet because async method not done yet, c) set with value or error from async method, d) repeat after new click.
FutureBuilder helps with both of these. My use of FutureBuilder does not handle re-enabling the button after the async operation completes. It wasn't clear to me whether that was desired.

Olaide Nojeem Ekeolere

unread,
Aug 31, 2018, 4:49:45 AM8/31/18
to mr...@google.com, warre...@gmail.com, Flutter Dev
I have this implemented without the issues you mentioned.

Olaide Nojeem Ekeolere

unread,
Aug 31, 2018, 4:52:56 AM8/31/18
to mr...@google.com, warre...@gmail.com, Flutter Dev
Oh, yeah, i noticed i omitted isMounted. You are right.

Mikkel Ravn

unread,
Aug 31, 2018, 4:57:15 AM8/31/18
to iamjav...@gmail.com, warre...@gmail.com, Flutter Dev
I don't have a simple way to deal with repeated execution using FutureBuilder. That widget relies on setState change to the Future-valued variable. Updating that asynchronously also requires us to check mounted manually.
Maybe there is a nice general Widget to be written here :-)

Reply all
Reply to author
Forward
0 new messages