Search code examples
flutterdartsetstategridview.builder

Why is setState not rebuilding my GridView.builder? How do I fix it?


I am trying to update a GridView.builder with setState and when the change is triggered, the actual state is updated behind the scene but the GridView.builder does not change until I manually refresh the app.

The setState function triggers and I have tested this. Here is the code:

import 'package:flutter/material.dart';
import 'image_display_card.dart';

void main() {
  runApp(MaterialApp(
    home: ChooseScreen(),
  ));
}


class ChooseScreen extends StatefulWidget {
  @override
  State<ChooseScreen> createState() => _ChooseScreenState();
}

class _ChooseScreenState extends State<ChooseScreen> {
  DisplayCard? currentSelectedCard;

  // The List of image cards
  List<DisplayCard> baseDisplayList = [];

  // These are dummy images I added
  List<Image> listOfInitialImages = const [
    Image(image: AssetImage('images/testing_stock/stickFigureMale.png')),
    Image(image: AssetImage('images/testing_stock/stickFigureFemale.png')),
    Image(image: AssetImage('images/testing_stock/gown.png')),
    Image(image: AssetImage('images/testing_stock/hat1.png')),
  ];

  @override
  void initState() {
    super.initState();
    baseDisplayList = [
      for (int i = 0; i < listOfInitialImages.length; i++)
        DisplayCard(
          picture: listOfInitialImages[i],
          onCardSelected: () {
            setCardToSelected(i);
          },
        ),
    ];
  }

  /// unselect the previous selected card and
  /// set currentSelectedCard to the new selected card.
  setCardToSelected(int index) {
    if (currentSelectedCard != null) {
      currentSelectedCard!.selectOrUnselect(false);
    }
    print('triggered');
    // set the new selected card.
    currentSelectedCard = baseDisplayList[index];
    currentSelectedCard!.selectOrUnselect(true);
    print('triggered again');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Choose picture'),
      ),
      body: GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
        ),
        itemCount: baseDisplayList.length,
        itemBuilder: (BuildContext context, int index) {
          return baseDisplayList[index];
        },
      ),
    );
  }
}

And this is display_card.dart:

import 'package:flutter/material.dart';

class DisplayCard extends StatelessWidget {
  final Image picture;
  final onCardSelected;
  bool isSelected;

  // TODO: Implement color change on selected picture
  DisplayCard({
    this.isSelected = false,
    this.onCardSelected,
    this.picture = const Image(
        image: AssetImage('images/testing_stock/stickFigureMale.png')),
  });

  selectOrUnselect(bool choice) {
    isSelected = choice;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onCardSelected,
      child: Container(
        height: 200,
        margin: const EdgeInsets.all(5),
        decoration: BoxDecoration(
          color: Colors.grey[300],
          image: DecorationImage(
            image: picture.image,
            colorFilter: ColorFilter.mode(
                Colors.black.withOpacity(isSelected ? 0.4 : 0.0),
                BlendMode.srcOver
            ),
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

I've edited it to contain only the necessary code to replicate the error. Sorry for before.

The state of baseDisplayList is updated but the GridView.builder is not rebuilt.

Ragarding how I know the GridView.builder isn't rebuilt, I tried changing the backgroundColor of the scaffold in the setCardToSelected function and the background color changed but the GridView didn't change. But when I refresh the app immediately afterwards, the GridView updated.

Please, how can I make the GridView.builder update when the setCardToSelected is called?

Thank you. I appreciate your help.


Solution

  • I tried using the provider package but I still got the same problem.

    It appears you have to build each individual item inside the GridView.builder for it to rebuild if there are any changes.

    Here is the solution I found to the problem.

    This is main.dart:

    
    import 'image_display_card.dart';
    
    void main() {
      runApp(MaterialApp(
        home: ChooseScreen(),
      ));
    }
    
    class ChooseScreen extends StatefulWidget {
      @override
      State<ChooseScreen> createState() => _ChooseScreenState();
    }
    
    class _ChooseScreenState extends State<ChooseScreen> {
      Image? currentlySelectedImage;
    
      // The List of image cards
      List<Image> baseImageList = [];
      // The list of image card states
      List<bool> baseStateList = [];
    
      // These are dummy images I added
      List<Image> listOfInitialImages = const [
        Image(image: AssetImage('images/testing_stock/stickFigureMale.png')),
        Image(image: AssetImage('images/testing_stock/stickFigureFemale.png')),
        Image(image: AssetImage('images/testing_stock/gown.png')),
        Image(image: AssetImage('images/testing_stock/hat1.png')),
      ];
    
      initialize() {
        for (int i = 0; i < listOfInitialImages.length; i++) {
          baseImageList.add(listOfInitialImages[i]);
          baseStateList.add(false);
        }
      }
    
      @override
      void initState() {
        super.initState();
        initialize();
      }
    
      /// Changes the currentlySelectedImage and the color of
      /// the chosen card by setting all values in the baseStatesList
      /// to false, then assigning only the chosen one to true.
      setToSelectedImage(int index) {
        currentlySelectedImage = baseImageList[index];
        setState(() {
          baseStateList.setAll(
              0, [for (int i = 0; i < baseStateList.length; i++) false]);
          baseStateList[index] = true;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
            ),
            itemCount: baseImageList.length,
            itemBuilder: (BuildContext context, int index) {
              return DisplayCard(
                picture: baseImageList[index],
                isSelected: baseStateList[index],
                onCardSelected: () {
                  setToSelectedImage(index);
                },
              );
            },
          ),
        );
      }
    }
    
    

    The DisplayCard class remains the same except that the selectOrUnselect method is no longer needed.

    The state of the gridview's contents actually update with setState if they are defined individually inside the gridview. This method is also shorter. Thanks for the help, guys.