你把逻辑放在 Flutter 的什么地方?

Where do you put logic in Flutter?

我正在尝试通过编写数独游戏来自学 Flutter/Dart。

我的计划是用一个名为“Square”的对象来代表数独网格上的 81 个方块中的每一个。

关于显示用户输入和记忆状态,每个 Square 都会管理自己的状态。到目前为止,我已经使用 StatefulWidget 对此进行了编程。

我被卡住的地方是还需要有一个顶级的游戏逻辑,它可以概述所有方块发生的事情并处理交互。这意味着我需要能够查询顶层的方块以了解它们的状态。

有什么方法可以用 Flutter 做到这一点吗?或者我需要用其他结构来处理它吗?

下面是我当前的 Square 实现的副本。

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

感谢您的帮助。

我会做一个二维的整数类型的顶层列表 `List squareValues = [[]];

然后制作一个 void 函数,用 -1(表示其中没有数字)或 null(但请记住 nullsafety)

来填充它们
void initSquares() {
   for loop over x {
       for loop over y {
           squareValues[x][y] = -1 // or null
       }
   }
}

然后您可以根据方形小部件内的回调更改这些值,告诉顶层值已更改

onSquareValueChanged: (value) {
    squareValues[squareX][squareY] = value;
    //update the screen with new values if necessary with setState

}

我找到了我的问题的有效答案,但我确信有不止一种方法可以做到。另外,我什至不确定我的做法是否是好的做法,因为我是 Flutter 的新手(来自 OO Java 和 React 背景)但在这里。

Square(如我的问题所示)在状态中不仅仅只有一个整数值。它有几个布尔值,几个整数,并且随着我找出更多属于 Square 的东西,它可能会获得更多。我最终调整了此处描述的模型 https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple#changenotifier

我创建了一个 SquareModel,其中包含管理 Square 的所有状态信息,以及需要它们的 getter 和 setter。 SquareModel 扩展 ChangeNotifier Setters 都包含 notifyListeners() 函数,该函数由 ChangeNotifier 提供,将通知所有侦听器(在本例中为 Squares)状态更改。 SquareModel 还包含一个初始化函数和 return 一个 ChangeNotifierProvider,其中 Square 作为构建函数的输出:

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

  
}

我有一个抽象 class LogicalBoard,它实例化所有 81 个 SquareModel 并将它们保存在静态列表中,以便应用程序中的任何其他对象都可以使用它们。 LogicalBoard 提供管理状态的方法并设置数独板背后的逻辑(例如,哪些 SquareModels 属于哪些列、行和框)。 LogicalBoard 还创建了一个包含 9 SudokuRow 个 StatefulWidget 的列表,每个 StatefulWidget 都有 9 个 Square 并以适当的间隔显示它们。设置数独板时会迭代此列表以显示板。

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

}

目前 LogicalBoard 中的大部分功能都与设置板有关。但是,请注意函数 setNumbersetSelectedSquare,它们演示了使用此结构进行状态管理是多么容易。

我想添加一个功能,用户点击一个 Square,Square 的边框颜色就会改变,以表明它是 'selected' Square。通过描述的 ChangeNotifier 设置,这只进行了三个小的更改:

  1. 我将函数 setSelectedSquare 添加到 LogicalBoard。它迭代所有 SquareModel 并确保所有 SquareModel 状态都具有 false 对应 _selected 除了用户单击的 Square 的 SquareModel 之外,它将具有 true 对应 _selected。它还将 LogicalBoard 上的 selectedSquare 的值设置为用户单击的 Square 的 SquareModel,以便记住它并可以在将来使用。
  2. 我用带有 onTap 函数的 Inkwell 小部件更新了 Square 的代码。请注意 Inkwell onTap 函数调用 LogicalBoard.setSelectedSquare.
  3. 我根据 _selected[ 的值制作了第一个 Container 小部件(它定义了方形的边框颜色)的颜色(我是英国人,我们拼写颜色 'colour') =54=]

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

LogicalBoard中的函数setNumber以类似的方式使用将所选方块(如果有的话)的状态更新为刚刚被点击的数字按钮上的数字.

顺便说一下,我发现 Flutter 很乐意在每次状态更改时重新渲染小部件,而不是试图尽量减少重新渲染(根据 React),这让我大开眼界