[Solved] How to use a variable parameter in a Flutter callback?

Question

Asked by WirelessG on December 17, 2021 (source).

I have a simplified flutter control, think of a row of 'radio' buttons or a menu bar. The parent passes in a list of 'captions' for each button and a callback. The control then hits the callback passing the index of the button tapped. The issue is, the 'buttons' are created dynamically and the quantity may vary by the parent. When I set the callback for the onTap function in GestureDetector, it will always hit the callback with the last value of the parameter (idx) in the loop. So if there are 4 buttons, the doCallback is always called with a 4, no matter which button is tapped. It appears like doCallback is being called with a reference to idx, rather than the value of idx. Is there a way to make each button send it's own index to the callback?

class CtrlRadioSelector extends StatelessWidget {
  CtrlRadioSelector({Key? key, required this.captions, required this.onTapItem})
      : super(key: key);
  final List<String> captions;
  final ValueSetter<int> onTapItem;

  @override
  Widget build(BuildContext context) {
    List<Widget> selectorItems = [];
    int idx = 0;
    for (var caption in captions) {
      selectorItems.add(Expanded(
          flex: 10,
          child: GestureDetector(
              onTap: () => doCallback(idx),
              child: Text(caption,
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18)))));
      idx++;
    }
    return Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: selectorItems);
  }

  void doCallback(int idx) {
    onTapItem(idx);
  }
}

Answer

Question answered by jamesdlin (source).

One fix would be use a for loop that iterates with an index, which you need anyway:

    for (var idx = 0; idx < captions.length; i += 1) {
      selectorItems.add(Expanded(
          flex: 10,
          child: GestureDetector(
              onTap: () => doCallback(idx),
              child: Text(captions[idx],
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18)))));
    }

This is because Dart specifically makes closures capture a for-loop's index (and not the values of all in-scope variables). Per the Dart Language Tour:

Closures inside of Dart’s for loops capture the value of the index, avoiding a common pitfall found in JavaScript. For example, consider:

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());

The output is 0 and then 1, as expected. In contrast, the example would print 2 and then 2 in JavaScript.

More generally, you also can just make sure that your closure refers to a variable that's local to the loop's body, which would avoid reassigning the referenced variable on each iteration. For example, the following also would work (although it would be unnecessarily verbose in your particular case):

    int idx = 0;
    for (var caption in captions) {
      var currentIndex = idx;

      selectorItems.add(Expanded(
          flex: 10,
          child: GestureDetector(
              onTap: () => doCallback(currentIndex),
              child: Text(caption,
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18)))));
      idx++;
    }
DART FLUTTER
SHARE: