I am trying to teach myself Flutter/Dart by programming a sudoku game.
My plan was to have an Object called "Square" to represent each of the 81 squares on the Sudoku grid.
Each Square would manage its own state as regards displaying user inputs and remembering state. So far, I have programmed this using a StatefulWidget.
Where I get stuck is that there also needs to be a top level of game logic which keeps an overview of what is happening with all the squares and deals with interactions. That means I need to be able to query the squares at top level to find out what their state is.
Is there any way to do this with Flutter? Or do I need to go about it with some other structure?
A copy of my current implementation of Square below.
import 'package:flutter/material.dart';
import 'package:sudoku_total/logical_board.dart';
class Square extends StatefulWidget {
final int squareIndex;
final int boxIndex;
final int rowIndex;
final int colIndex;
const Square(
this.squareIndex,
this.boxIndex,
this.rowIndex,
this.colIndex, {
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _SquareState();
}
class _SquareState extends State<Square> {
int _mainNumber = 0;
bool showEdit1 = false;
bool showEdit2 = false;
bool showEdit3 = false;
bool showEdit4 = false;
bool showEdit5 = false;
bool showEdit6 = false;
bool showEdit7 = false;
bool showEdit8 = false;
bool showEdit9 = false;
bool _selected = false;
bool _selectedCollection = false;
@override
Widget build(BuildContext context) {
LogicalBoard.numberButtonNotifier.addListener(() {
if(LogicalBoard.selectedSquare?.squareIndex == widget.squareIndex){
setState(() {
_mainNumber = LogicalBoard.numberLastClicked;
});
}
});
LogicalBoard.selectionNotifier.addListener(() {
setState(() {
_selected =
LogicalBoard.selectedSquare?.squareIndex == widget.squareIndex;
_selectedCollection =
LogicalBoard.selectedSquare?.squareIndex == widget.squareIndex ||
LogicalBoard.selectedSquare?.rowIndex == widget.rowIndex ||
LogicalBoard.selectedSquare?.colIndex == widget.colIndex ||
LogicalBoard.selectedSquare?.boxIndex == widget.boxIndex;
});
});
return Material(
child: InkWell(
onTap: () => LogicalBoard.selectionNotifier
.setSelectedSquare(widget.squareIndex),
child: Container(
padding: const EdgeInsets.all(2.0),
color: _selected
? Theme.of(context).errorColor
: Theme.of(context).primaryColorDark,
width: 52.0,
height: 52.0,
child: Container(
padding: _mainNumber == 0
? const EdgeInsets.fromLTRB(2.0, 8.0, 2.0, 2.0)
: const EdgeInsets.all(0.0),
color: _selectedCollection
? Theme.of(context).backgroundColor
: Theme.of(context).primaryColor,
width: 48.0,
height: 48.0,
child: _mainNumber != 0
? Center(
child: Text(
_mainNumber.toString(),
style: Theme.of(context).textTheme.headline3,
))
: Column(mainAxisSize: MainAxisSize.min, children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
showEdit1 ? "1" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
showEdit2 ? "2" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
showEdit3 ? "3" : "",
style: Theme.of(context).textTheme.bodyText2,
))
])),
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
showEdit4 ? "4" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
showEdit5 ? "5" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
showEdit6 ? "6" : "",
style: Theme.of(context).textTheme.bodyText2,
))
])),
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
showEdit7 ? "7" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
showEdit8 ? "8" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
showEdit9 ? "9" : "",
style: Theme.of(context).textTheme.bodyText2,
))
]))
])),
)));
}
}
Thanks for your help.
CodePudding user response:
I would do a toplevel List of type Integer that is 2-dimensional `List<List> squareValues = [[]];
then make a void function that fills these with lets say -1 (meaning no number in there) or null
(but keep nullsafety in mind)
void initSquares() {
for loop over x {
for loop over y {
squareValues[x][y] = -1 // or null
}
}
}
Then you could change these values based on callbacks inside your square widget that tells the top level that the value has been changed
onSquareValueChanged: (value) {
squareValues[squareX][squareY] = value;
//update the screen with new values if necessary with setState
}
CodePudding user response:
I found an answer to my question that works, though I'm sure there is more than one way to do it. Also, I'm not even sure if mine is good practice or not, as I am new to Flutter (coming from an OO Java and React background) but here it is.
Square (as seen in my question) has a lot more than just one integer value in state. It has several booleans, several integers and is likely to gain more as I figure out more things that belong in Square. I ended up adapting the model described here https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple#changenotifier
I created a SquareModel which contains all the state information to manage a Square, along with getters and setters where they will be needed.
SquareModel extends ChangeNotifier
Setters all contain the function notifyListeners()
, which is provided by ChangeNotifier and will notify all the listeners (in this case, the Squares) of state changes.
SquareModel also contains a function to initialise and return a ChangeNotifierProvider with Square as the output from the builder function:
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:sudoku_total/square.dart';
import 'package:sudoku_total/square_collection.dart';
class SquareModel extends ChangeNotifier {
final int squareIndex;
final int rowIndex;
final int colIndex;
final int boxIndex;
LogicalBox? box;
LogicalRow? row;
LogicalCol? col;
SquareModel(this.squareIndex, this.rowIndex, this.colIndex, this.boxIndex);
int? _mainNumber;
int? _answer;
bool _showEdit1 = false;
bool _showEdit2 = false;
bool _showEdit3 = false;
bool _showEdit4 = false;
bool _showEdit5 = false;
bool _showEdit6 = false;
bool _showEdit7 = false;
bool _showEdit8 = false;
bool _showEdit9 = false;
bool _selected = false;
bool _selectedCollection = false;
set editNumber(value) {
switch (value) {
case 1:
_showEdit1 = !_showEdit1;
break;
case 2:
_showEdit2 = !_showEdit2;
break;
case 3:
_showEdit3 = !_showEdit3;
break;
case 4:
_showEdit4 = !_showEdit4;
break;
case 5:
_showEdit5 = !_showEdit5;
break;
case 6:
_showEdit6 = !_showEdit6;
break;
case 7:
_showEdit7 = !_showEdit7;
break;
case 8:
_showEdit8 = !_showEdit8;
break;
case 9:
_showEdit9 = !_showEdit9;
}
notifyListeners();
}
List<int> getEditNumbers() {
List<int> editNumbers = [];
if (_showEdit1) {
editNumbers.add(1);
}
if (_showEdit2) {
editNumbers.add(2);
}
if (_showEdit3) {
editNumbers.add(3);
}
if (_showEdit4) {
editNumbers.add(4);
}
if (_showEdit5) {
editNumbers.add(5);
}
if (_showEdit6) {
editNumbers.add(6);
}
if (_showEdit7) {
editNumbers.add(7);
}
if (_showEdit8) {
editNumbers.add(8);
}
if (_showEdit9) {
editNumbers.add(9);
}
return editNumbers;
}
int? get mainNumber => _mainNumber;
set mainNumber(int? value) {
_mainNumber = value;
notifyListeners();
}
int? get answer => _answer;
set answer(int? value) {
_answer = value;
notifyListeners();
}
bool get selected => _selected;
set selected(bool value) {
_selected = value;
notifyListeners();
}
bool get selectedCollection => _selectedCollection;
set selectedCollection(bool value) {
_selectedCollection = value;
notifyListeners();
}
//This last function is called to instantiate the ChangeNotifierProvider for each Square and make sure each Square is provided with the relevant SquareModel state whenever state changes.
getSquare() => ChangeNotifierProvider(
create: (context) => this,
child: Consumer<SquareModel>(
builder: (context, square, child) => Square(
squareIndex: squareIndex,
mainNumber: _mainNumber,
answer: _answer,
selected: _selected,
selectedCollection: _selectedCollection,
showEdit1: _showEdit1,
showEdit2: _showEdit2,
showEdit3: _showEdit3,
showEdit4: _showEdit4,
showEdit5: _showEdit5,
showEdit6: _showEdit6,
showEdit7: _showEdit7,
showEdit8: _showEdit8,
showEdit9: _showEdit9,
),
));
}
I have an abstract class LogicalBoard
which instantiates all 81 of the SquareModels and holds them in a static List so they are available to any other object in the application. LogicalBoard
provides methods to manage the state and sets up the logic behind the sudoku board (for example, which SquareModels belong to which columns, rows and boxes). LogicalBoard also creates a list of 9 SudokuRow
which are a StatefulWidget, each of which takes 9 Squares and displays them with the appropriate gaps. This list is iterated when the Sudoku board is set up in order to display the board.
import 'package:sudoku_total/square_model.dart';
import 'package:sudoku_total/sudoku_row.dart';
import 'package:sudoku_total/square_collection.dart';
class LogicalBoard {
static final List<SquareModel> squareModels = _initSquares();
static final List<SudokuRow> rowsWidgets = _initRowsWidgets();
static final List<LogicalBox> boxes = _initBoxes();
static final List<LogicalRow> rows = _initRows();
static final List<LogicalCol> cols = _initCols();
static SquareModel? selectedSquare;
static int numberLastClicked=0;
static void setNumber(int number){
if(selectedSquare != null){
selectedSquare?.mainNumber = number;
}
}
static void setSelectedSquare(int squareIndex){
selectedSquare = squareModels[squareIndex];
for (var sm in squareModels) {
sm.selected = (sm.squareIndex==squareIndex);
sm.selectedCollection = sm.boxIndex==selectedSquare?.boxIndex || sm.rowIndex==selectedSquare?.rowIndex || sm.colIndex==selectedSquare?.colIndex;
}
}
static List<LogicalBox> _initBoxes(){
List<LogicalBox> boxes = [];
for(var i = 0; i<9; i ){
boxes.add(LogicalBox(_getBoxSquares(i)));
}
return List.unmodifiable(boxes);
}
static List<LogicalRow> _initRows(){
List<LogicalRow> rows = [];
for(var i = 0; i<9; i ){
rows.add(LogicalRow(_getRowSquares(i)));
}
return List.unmodifiable(rows);
}
static List<LogicalCol> _initCols(){
List<LogicalCol> boxes = [];
for(var i = 0; i<9; i ){
boxes.add(LogicalCol(_getColSquares(i)));
}
return List.unmodifiable(boxes);
}
static List<SudokuRow> _initRowsWidgets() {
List<SudokuRow> rows = [];
for(var i = 0; i<9; i ){
rows.add(SudokuRow(_getRowSquares(i), i));
}
return List.unmodifiable(rows);
}
static List<SquareModel> _getRowSquares(int rowIndex){
return List.unmodifiable(squareModels.where((element) => element.rowIndex == rowIndex));
}
static List<SquareModel> _getColSquares(int colIndex){
return List.unmodifiable(squareModels.where((element) => element.colIndex == colIndex));
}
static List<SquareModel> _getBoxSquares(int boxIndex){
return List.unmodifiable(squareModels.where((element) => element.boxIndex == boxIndex));
}
static List<SquareModel> _initSquares() {
List<SquareModel> initSquares = [];
//Initialise squares with squareIndex, rowIndex, colIndex and boxIndex
int squareIndex = 0;
int rowIndex = 0;
int colStartIndex = 0;
int colIndex = 0;
int boxStartIndex = 0;
int boxIndex = 0;
for (var count = 1; count < 82; count ) {
//Add row, col and box index to the new square
initSquares.add(SquareModel(squareIndex, rowIndex, colIndex, boxIndex));
//Every square
//col index increments
colIndex ;
//square index increments
squareIndex ;
//Every 3 squares
if (count % 3 == 0) {
//Box index increments
boxIndex ;
}
//Every 9 squares
if (count % 9 == 0) {
//col index goes back to start
colIndex = colStartIndex;
//row index increments
rowIndex ;
//Box index goes back to the start
boxIndex = boxStartIndex;
}
//Every 27 squares
if (count % 27 == 0) {
//Box start index increments by 3
boxStartIndex = boxStartIndex 3;
boxIndex = boxStartIndex;
}
}
return List.unmodifiable(initSquares);
}
}
Most of the functions in LogicalBoard at the moment are related to setting up the board. However, notice the functions setNumber
and setSelectedSquare
which demonstrate how easily the state management works with this structure.
I wanted to add functionality whereby a user clicks on a Square, and the border colour of the Square changes to show that it is the 'selected' Square. With the ChangeNotifier setup described, this only took three small changes:
- I added the function
setSelectedSquare
to LogicalBoard. It iterates all the SquareModels and makes sure all the SquareModel states havefalse
for_selected
except the SquareModel for the Square the user clicked, which will havetrue
for_selected
. It also sets the value of selectedSquare on LogicalBoard to the SquareModel for the Square the user clicked so that it is remembered and can be used in future. - I updated the code for
Square
with an Inkwell widget with an onTap function. Notice that the InkwellonTap
function calls LogicalBoard.setSelectedSquare. - I made the colour (I'm British, we spell colour 'colour') of the first Container widget (which defines the border colour of the Square) dependent on the value of
_selected
Here is the code for Square
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'logical_board.dart';
class Square extends StatelessWidget {
final int _squareIndex;
final int? _mainNumber;
final int? _answer;
final bool _showEdit1;
final bool _showEdit2;
final bool _showEdit3;
final bool _showEdit4;
final bool _showEdit5;
final bool _showEdit6;
final bool _showEdit7;
final bool _showEdit8;
final bool _showEdit9;
final bool _selected;
final bool _selectedCollection;
const Square({
Key? key,
required int squareIndex,
required int? mainNumber,
required int? answer,
required bool showEdit1,
required bool showEdit2,
required bool showEdit3,
required bool showEdit4,
required bool showEdit5,
required bool showEdit6,
required bool showEdit7,
required bool showEdit8,
required bool showEdit9,
required bool selected,
required bool selectedCollection,
}) : _squareIndex = squareIndex,
_mainNumber = mainNumber,
_answer = answer,
_showEdit1 = showEdit1,
_showEdit2 = showEdit2,
_showEdit3 = showEdit3,
_showEdit4 = showEdit4,
_showEdit5 = showEdit5,
_showEdit6 = showEdit6,
_showEdit7 = showEdit7,
_showEdit8 = showEdit8,
_showEdit9 = showEdit9,
_selected = selected,
_selectedCollection = selectedCollection,
super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
onTap: () => LogicalBoard.setSelectedSquare(_squareIndex),
child: Container(
padding: const EdgeInsets.all(2.0),
color: _selected
? Theme.of(context).errorColor
: Theme.of(context).primaryColorDark,
width: 52.0,
height: 52.0,
child: Container(
padding: _mainNumber == null
? const EdgeInsets.fromLTRB(2.0, 8.0, 2.0, 2.0)
: const EdgeInsets.all(0.0),
color: _selectedCollection
? Theme.of(context).backgroundColor
: Theme.of(context).primaryColor,
width: 48.0,
height: 48.0,
child: _mainNumber != null
? Center(
child: Text(
_mainNumber.toString(),
style: Theme.of(context).textTheme.headline3,
))
: Column(mainAxisSize: MainAxisSize.min, children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
_showEdit1 ? "1" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
_showEdit2 ? "2" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
_showEdit3 ? "3" : "",
style: Theme.of(context).textTheme.bodyText2,
))
])),
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
_showEdit4 ? "4" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
_showEdit5 ? "5" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
_showEdit6 ? "6" : "",
style: Theme.of(context).textTheme.bodyText2,
))
])),
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
_showEdit7 ? "7" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
_showEdit8 ? "8" : "",
style: Theme.of(context).textTheme.bodyText2,
)),
Flexible(
child: Text(
_showEdit9 ? "9" : "",
style: Theme.of(context).textTheme.bodyText2,
))
]))
])),
)));
}
}
The function setNumber
in LogicalBoard
is used in a similar way to update the state of the selected Square (if there is one) to the number on a number button which has just been clicked.
By the way, it was a complete eye-opener to me to find that Flutter is quite happy to re-render the widgets every state change rather than trying to minimise re-renders (as per React)