Before, I used ListBuilder
to generate a list of 70 numbers and it worked but it took a long time to generate the 70 numbers into custom widgets and also when I tapped a number just to change the background color state, it took a couple of milliseconds before the state being changed.
Now I am using a FutureBuilder
to be able to load the screen while waiting for the generated 70 integers. But when I tap on the ball number, the background color is not updated ... It's like setState()
is not working in Future ListBuilder.
This question: "Flutter - How to update state (or value?) of a Future/List used to build ListView (via FutureBuilder)" is very similar, but it did not solve my problem.
Here is the code I have in the build method
Flexible(
child:FutureBuilder<List<Widget>>(
future: ballNumbers,
builder: (context, snapshot){
if(snapshot.connectionState != ConnectionState.done){
return Center(child: CircularProgressIndicator());
}
if(snapshot.hasError){
return Center(child: Text("An error has occured"));
}
List<Widget> balls = snapshot.data ?? [];
return GridView.count(
crossAxisCount: 9,
children: balls,
);
}
)
Here is how I start the state for the function:
Future<List<Widget>> ballNumbers;
List<int> picks = [];
@override
void initState() {
ballNumbers = getBallNumbers();
});
Future<List<Widget>> getBallNumbers() async {
return List.generate(limitBallNumber,(number){
number = number + 1;
return Padding(
padding:EdgeInsets.all(2.5),
child:Ball(
number : number,
size: ballWidth,
textColor:(picks.contains(number)) ? Colors.black : Colors.white,
ballColor: (picks.contains(number)) ? Style.selectedBallColor : Style.ballColor,
onTap:(){
setState((){
picks.contains(number) ? picks.remove(number) : picks.add(number);
});
}
)
);
});
}
UPDATED: Here is the class the Ball
widget
class Ball extends StatelessWidget {
final Color ballColor;
final Color textColor;
final double size;
final double fontSize;
final int number;
final VoidCallback onTap;
Ball({Key key, @required this.number,
this.textColor,
this.ballColor,
this.onTap,
this.size = 55.0,
this.fontSize = 14,
}) : super(key : key);
@override
Widget build(BuildContext context) {
return Container(
height: size,
width: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Style.secondaryColor,
ballColor != null ? ballColor : Style.ballColor,
],
begin: Alignment.bottomLeft,
end: Alignment.topRight
)
),
child: FlatButton(
padding: EdgeInsets.all(0),
child: Container(
child: Text(
number.toString().length > 1 ? number.toString() : "0" + number.toString(),
style: TextStyle(
fontSize: fontSize,
color: textColor != null ? textColor : Colors.white
),
),
padding: const EdgeInsets.all(4.0),
decoration:BoxDecoration(
color: Colors.transparent,
border: Border.all(color: textColor != null ? textColor : Colors.white, width: 1),
borderRadius: BorderRadius.circular(32),
)
),
color: Colors.transparent,
onPressed: onTap,
),
);
}
}
The issue is that getBallNumbers
is only being called once in initState
, so when picks
is updated, it doesn't matter because getBallNumbers
isn't called again to update the colors being passed to the Ball
widgets.
A simple fix would be to call getBallNumbers
in your build
with future: getBallNumbers()
, but this would lead to the CircularProgressIndicator
being shown on every click as the List
regenerates.
However, ideally, you should be handling all of the color changing within the state of each Ball
so that you're not forced to rebuild that List
on every click. And to maintain a List
of selected numbers within the parent widget's State
, you should pass a callback to each ball that adds and removes their number from the List
in the parent.
Rough example:
Ball class(modified to be stateful and removed parameters that became unecessary; active state is now stored within the ball instead of solely in the parent):
class Ball extends StatefulWidget {
final double size;
final double fontSize;
final int number;
final VoidCallback toggleBall;
final bool initialActiveState;
Ball({Key key, @required this.number,
this.toggleBall,
this.size = 55.0,
this.fontSize = 14,
this.initialActiveState,
}) : super(key : key);
_BallState createState() => _BallState();
}
class _BallState extends State<Ball> {
bool isActive;
@override
void initState() {
super.initState();
isActive = widget.initialActiveState;
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Style.secondaryColor,
isActive ? Style.selectedBallColor : Style.ballColor,
],
begin: Alignment.bottomLeft,
end: Alignment.topRight
)
),
child: FlatButton(
padding: EdgeInsets.all(0),
child: Container(
child: Text(
widget.number.toString().length > 1 ? widget.number.toString() : "0" + widget.number.toString(),
style: TextStyle(
fontSize: widget.fontSize,
color: isActive ? Colors.black : Colors.white,
),
),
padding: const EdgeInsets.all(4.0),
decoration:BoxDecoration(
color: Colors.transparent,
border: Border.all(color: isActive ? Colors.black : Colors.white, width: 1),
borderRadius: BorderRadius.circular(32),
)
),
color: Colors.transparent,
onPressed: () {
if(!isActive && widget.activeBallList.length >= 7) {
return;
}
setState(() {
isActive = !isActive;
});
widget.activeBallList.contains(widget.number) ? widget.activeBallList.remove(widget.number) : widget.activeBallList.add(widget.number);
},
),
);
}
}
Parent class(the only part that needs to be modified is the parameters for Ball
):
Future<List<Widget>> getBallNumbers() async {
return List.generate(limitBallNumber,(number){
number = number + 1;
return Padding(
padding:EdgeInsets.all(2.5),
child: Ball(
number: number,
size: ballWidth,
initialActiveState: picks.contains(number),
activeBallList: picks,
)
);
});
}