0

App is a simple memory/guessing game with a grid of squares. Floating action button triggers a "New game" dialog, and a Yes response triggers setState() on the main widget. The print() calls show it is building all the Tile widgets in the grid, but as it returns, the old grid values are still showing. Probably done something stupid but not seeing it. Basic code is below. TIA if anyone can see what is missing/invalid/broken/etc.

Main.dart is the usual main() that creates a stateless HomePage which creates a stateful widget which uses this State:

class MemHomePageState extends State<MemHomePage> {

  GameBoard gameBoard = GameBoard();
  GameController? gameController;
  int gameCount = 0, winCount = 0;

  @override
  void initState() {
    super.initState();
    gameController = GameController(gameBoard, this);
  }

  @override
  Widget build(BuildContext context) {
    if (kDebugMode) {
      print("MemHomepageState::build");
    }
    gameBoard.newGame(); // Resets secrets and grids
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: GridView.count(
          crossAxisCount: Globals.num_columns,
          children: List.generate(Globals.num_columns * Globals.num_rows, (index) {
            int x = index~/Globals.NR, y = index%Globals.NR;
            int secret = gameBoard.secretsGrid![x][y];
            var t = Tile(x, y, Text('$secret'), gameController!);
            gameBoard.tilesGrid![x].add(t);
            if (kDebugMode) {
              print("Row $x is ${gameBoard.secretsGrid![x]} ${gameBoard.tilesGrid![x][y].secret}");
            }
            return t;
          }),
        ),
        // Text("You have played $gameCount games and won $winCount."),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => newGameDialog("Start a new game?"),
        tooltip: 'New game?',
        child: const Icon(Icons.refresh_outlined),
      ),
    );
  }

  /** Called from the FAB and also from GameController "won" logic */
  void newGameDialog(String message) {
    showDialog<void>(
        context: context,
        barrierDismissible: false, // means the user must tap a button to exit the Alert Dialog
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text("New game?"),
            content: Text(message),
            //),
            actions: <Widget>[
              TextButton(
                child: const Text('Yes'),
                onPressed: () {
                  setState(() {
                    gameCount++;
                  });
                  Navigator.of(context).pop();
                },
              ),
              TextButton(
                child: const Text('No'),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          );
        }
    );
  }

The Tile class is a StatefulWidget whose state determines what that particular tile should show:

import 'package:flutter/material.dart';
import 'gamecontroller.dart';
    
enum TileMode {
      SHOWN,
      HIDDEN,
      CLEARED,
    }
    
/// Represents one Tile in the game
class Tile extends StatefulWidget {
  final int x, y;
  final Widget secret;
  final GameController gameController;
  TileState? tileState;

  Tile(this.x, this.y, this.secret, this.gameController, {super.key});

  @override
  State<Tile> createState() => TileState(x, y, secret);
    
  setCleared() {
    tileState!.setCleared();
  }
}
    
    class TileState extends State<Tile> {
      final int x, y;
      final Widget secret;
      TileMode tileMode = TileMode.HIDDEN;
    
      TileState(this.x, this.y, this.secret);
    
      _unHide() {
        setState(() => tileMode = TileMode.SHOWN);
        widget.gameController.clicked(widget);
      }
    
      reHide() {
        print("rehiding");
        setState(() => tileMode = TileMode.HIDDEN);
      }
    
      setCleared() {
        print("Clearing");
        setState(() => tileMode = TileMode.CLEARED);
      }
    
      _doNothing() {
        //
      }
    
      @override
      Widget build(BuildContext context) {
        switch(tileMode) {
          case TileMode.HIDDEN:
            return ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.teal,
                ),
                onPressed: _unHide,
                child: Text(''));
          case TileMode.SHOWN:
            return ElevatedButton(
                onPressed: _doNothing,
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green,
                ),
                child: secret);
          case TileMode.CLEARED:
            return ElevatedButton(
                onPressed: _doNothing,
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.black12,
                ),
                child: const Icon(Icons.check));
        }
      }
    }
idarwin
  • 607
  • 4
  • 19

2 Answers2

0

it looks like you are calling the following in your build function. That would cause everything to reset everytime it builds. Perhaps it belongs in init instead?

gameBoard.newGame(); // Resets secrets and grids
user1805015
  • 333
  • 1
  • 2
  • 6
  • Thanks @user1805015 but that basically re-randomizes the secretsGrid data structure that is used here in the middle of the main widget build function: int secret = gameBoard.secretsGrid![x][y]; The main widget is only rebuilt when a new game is started. – idarwin Nov 29 '22 at 02:17
  • I did try moving the call into initState() as you suggested, and then the printouts show that same board is being used, that is, the randomization call really belongs in build, not in initstate(). Thanks. – idarwin Nov 29 '22 at 02:25
  • Perhaps it has something to do with the order in which you are popping the dialog and building the state. Try popping the dialog and then calling set state afterwards. – user1805015 Nov 29 '22 at 02:35
  • Seemed like a good thought, and I'll leave the code as you suggested, but the problem isn't solved. BTW there is a related issue; after using either hot reload or the New Game feature, after a few moves it starts reporting "setState() called in constructor". I'll post the stacktrace for that in a minute or so. – idarwin Nov 29 '22 at 02:40
  • @override TileState createState() { //ignore: no_logic_in_create_state return tileState!; } should be State createStage() { return TileState(); } – user1805015 Nov 29 '22 at 02:50
  • That's a valid correction. I need to do some cleanup there in addition to that change... Thanks. – idarwin Nov 29 '22 at 02:54
  • I actually mean make sure you aren't accidently caching the old state and reusing it instead of creating a new state when it builds – user1805015 Nov 29 '22 at 02:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249992/discussion-between-idarwin-and-user1805015). – idarwin Nov 29 '22 at 15:14
0

The original problem is that the Tile objects, although correctly created and connected to the returned main widget, did not have distinct 'key' values so they were not replacing the originals. Adding 'key' to the Tile constructor and 'key: UniqueKey()' to each Tile() in the loop, solved this problem. It exposed a related problem but is out of scope for this question. See the github link in the OP for the latest version.

idarwin
  • 607
  • 4
  • 19