📚

【Flutter】GestureDetectorを使用したカスタムボタンで async/await にならないとき

に公開
3

よく、ボタンでこういうことやりたいですよね。

MyCustomButton(
  onTap: () async {
    await doSomething();
  },
)

でも待ってください、
このボタンがカスタムWidgetの場合、async/await にならないことがあります。

たとえば、次のようにGestureDetectorを使用していて、onTapが Function()? 型で定義されていると、
onTap の中で非同期処理を実行できないです。

class MyButton extends StatelessWidget {
  final Function()? onTap;
  const MyButton({this.onTap});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: ...
    );
  }
}

これを async/await に対応させるには、
コールバックを Future を返す関数 に変更する必要があります。

class MyButton extends StatelessWidget {
  final Future<void> Function()? onTap;
  const MyButton({this.onTap});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () async {
        // 二重タップ防止などの処理をここに入れられます
        await onTap?.call();
      },
      child: ...
    );
  }
}

Future<void> Function()は、AsyncValueCallbackでも表せるので、
以下でもOKです。

class MyButton extends StatelessWidget {
  final AsyncCallback? onTap;
  const MyButton({this.onTap});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () async {
        // 二重タップ防止などの処理をここに入れられます
        await onTap?.call();
      },
      child: ...
    );
  }
}

まとめ

カスタムWidgetのコールバックを Function() にしてしまうと、
async/await が使えず非同期処理が正しく動かないことがあります。

そのため、非同期を考慮した汎用的な設計をするなら、
AsyncCallback?(または Future<void> Function()?)を使うのが良いと思います。

カスタムWidgetにFunctionのパラメータ作る時は、とりあえずFutureにしとくのが良いと思っています。
カスタムじゃなくてFlutter標準のWidgetだとFuture FunctionもただのFunctionもどちらも渡せるのでややこしいんですよね。

よくonTapがFutureなっていないカスタムボタン系Widgetを見かけるので、皆さんも気をつけてください!

追記

Future明示していないFunctionをカスタムボタンに渡しているのにasync/awaitできているっぽい挙動になっているコードがあったので見てみたら、中身がCupertinoButtonだった。
GestureDetectorで完全カスタムボタンにしている場合だけの現象のようですね。

Discussion

FBFB

しずみやさん
先日の記事に引き続き、コメント失礼致します。

記事の本題とはずれますが、nullable 関数を呼び出す場合以下の書き方だとコードを減らせます。
onTapがnullだったら呼び出されない想定です。

onTap?.call();
しずみやしずみや

ありがとうございます、修正しました!

FBFB

しずみやさんの日々の開発の力になれば幸いです!