WirelessG
WirelessG

Reputation: 229

How to use a variable parameter in a Flutter callback?

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);
  }
}

Upvotes: 3

Views: 835

Answers (2)

jamesdlin
jamesdlin

Reputation: 90174

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++;
    }

Upvotes: 1

Shourya Shikhar
Shourya Shikhar

Reputation: 1524

This is the correct way to create a dynamic row with buttons, where the actual index of the children is preserved:

import 'package:flutter/material.dart';

class CtrlRadioSelector extends StatelessWidget {
  const 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) {

    return Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,

        children: List.generate(  //equivalent to your code using for loop and a list
            captions.length, //if length of captions is 4, it'll iterate 4 times
            (idx) {

          return Expanded(
              flex: 10,
              child: GestureDetector(
                  onTap: () => doCallback(
                      idx), //value of idx is the actual index of the button
                  child: Text(captions[idx],
                      textAlign: TextAlign.center,
                      style: const TextStyle(fontSize: 18))));
        }));
  }

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

Upvotes: 1

Related Questions