【Flutter】コールバック関数を別のWidgetで呼び出す
サンプルを用いて、タイトルの通り別の Widget でコールバック関数を呼び出しています。
おまけでコールバック関数について少し触れています。
コールバックを寿司屋で表現
SushiShopというStatefulWidget内で個別のSushiWidgetをいくつも表示させたいとします。
また寿司は作ったり食べたりできるので、makeSushiとeatSushiという関数も用意します。
makeSushiは新しく寿司を作り一覧に追加し、eatSushiは選んだ(タップした)寿司を消す挙動にします。
SushiWidgetを一覧化するのはかんたんで、SushiShop内のビルドの通りWidgetのchildrenとしてリストを渡してあげればよいです。リスト内のオブジェクトの型はSushiクラスとして、インスタンスを格納します。
Sushiクラスのプロパティには、ネタとしてneta(サンプルコードは"鮪"確定ですが…)と、あとはどの寿司を食べたか識別するための id を持っています。
SushiShop内で寿司をつくるmakeSushi関数を作成し、idとnetaを初期化します。あとは寿司を食べるeatSushi関数を…とここで問題がでてきます。
void eatSushi(int netaId) {sushiList.removeAt(netaId);}で一見よさそうにみえますが、eatSushiを発動させるのは寿司がタップされたとき、すなわちSushiShopクラスとは違うSushiクラスのウィジェット(TextButton)がタップされたときになります。
そのためSushiWidget内でonPressed: _eatSushi()みたいなことはできないんです。
これを解決するために、SushiクラスではFunction(int) sushiCallback(引数にint型を必要とする関数)をプロバティとして持たせています。
寿司自身がタップされたときにはonPressed: () => sushiCallback(id)としてid(=識別するためのもの)を引数に渡してあげることで、SushiShopでどの寿司がタップされたのかを判断することができるのです。
class SushiShop extends StatefulWidget {
...
}
class SushiShopState extends State<SushiShop> {
List<Sushi> sushiList = [];
int _sushiCounter = 0;
void _makeSushi() {
  Sushi sushi = Sushi(
    id: _sushiCounter,
    neta: "鮪",
    sushiCallback: (sushiId) => _eatSushi(sushiId),
  );
  _sushiCounter++;
  setState(() {
    sushiList.add(sushi);
  });
}
void _eatSushi(int netaId) {
  _sushiCounter--;
  setState(() {
    // 指定した添字のオブジェクトを削除
    sushiList.removeAt(netaId);
  });
}
Widget build(BuildContext context) {
  return Column(
    children: [
      IconButton(
        ...,
        onPressed: () => _makeSushi(),
      ),
      Column(
        ...
        children: sushiList,
      ),
    ],
  ),
}
class Sushi extends StatelessWidget {
  Sushi({
    required this.id,
    required this.neta,
    required this.sushiCallback,
    Key? key
  }) : super(key: key);
  int id;
  String neta;
  Function(int) sushiCallback;
  
  Widget build(BuildContext context) {
    return Center(
      child: TextButton(
        onPressed: () => sushiCallback(id),
        child: Text(
          "$neta",
        ),
      ),
    );
  }
}
寿司を並べるくらいならTextとかを羅列すれば良かったのですが、任意にタップした寿司を消すという動的な仕様を実装しようと思ったらここまでややこしくなりました。
※sushiCounterのインクリメントなどはもうちょっといじらないとうまく動作しません。
おまけ
寿司クラスも、JS でかんたんにまとめると少しわかりやすいように思えます。
// 寿司関数を定義(高階関数)
function sushi(id, neta, func) {
  func(id)
}
// コールバック関数
const eatSushi = function(id) {
  ... // [id]の寿司をたべる処理
}
sushi(1, "サーモン", eatSushi);
コールバック関数は直接引数にできるので…
function sushi(id, neta, func) {
  func(id)
}
sushi(1, "サーモン", function(id){
  ...
});
これにアロー関数を用いると…
function sushi(id, neta, func) {
  func(id)
}
sushi(1, "サーモン", (id) => {...});
サンプルコードのこれも
// 高階関数
Sushi({
  required this.id,
  required this.neta,
  required this.sushiCallback,
  Key? key
}) : super(key: key);
// コールバック関数
void _eatSushi(int netaId) {
  _sushiCounter--;
  setState(() {
    sushiList.removeAt(netaId);
  });
}
Sushi sushi = Sushi(
  id: _sushiCounter,
  neta: "鮪",
  sushiCallback: (sushiId) => _eatSushi(sushiId),
);
つまりはこういうこと(一部省略)
Sushi(id, neta, sushiCallback)
void _eatSushi(int netaId) {
  sushiList.removeAt(netaId);
}
Sushi sushi = Sushi(_sushiCounter, "鮪", (sushiId) => _eatSushi(sushiId));
Widgetで見かけるコロンや名前付き引数の波括弧がいい感じにややこしくしてますね…。
書き方や考え方にいろいろと甘い部分はあると思います。
Flutterは楽しいのでもっと勉強してパワーアップしたいです 🦋
Discussion