Reputation: 229
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
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
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