Search code examples
flutterflutter-widgetflutter-futurebuilder

How to update widget state built inside FutureBuilder


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.

ow

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

Solution

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