Flutterでビンゴを作成する

## 概要
Flutterでビンゴを作成する

###  実行環境
#### Mac
```sh
$ sw_vers
ProductName:    macOS
ProductVersion: 11.6
BuildVersion:   20G165
```

#### Flutter
```sh
$ flutter --version
Flutter 2.5.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision ffb2ecea52 (5 days ago) • 2021-09-17 15:26:33 -0400
Engine • revision b3af521a05
Tools • Dart 2.14.2
```

### 仕様
- ビンゴは5x5マス
- アルファベットを重複なしでランダムにマスに配置する
- リセットボタンを押すと最初に戻る
- 抽選ボタンを押すとアルファベットが1文字抽選される
- 縦横斜めいずれかに5マス連続で揃うとビンゴ
- 抽選履歴が表示される

#### 完成予定図
![BINGO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/c29b8765-432c-0a01-f924-543e863637d3.png)

## 手順
### Flutterのプロジェクトを作成
```sh
flutter create --org xxx.xxx.xxx bingo
```

### 作成したプロジェクトに移動
```sh
cd bingo
```

## アプリの内容
### メイン処理
```dart:lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';

void main() => runApp(BingoApp());

class BingoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text('Bingo'),
          ),
          body: _BingoHome(),
        ),
      );
}
```

### 状態管理(StatefulWidget)
```dart
class _BingoHome extends StatefulWidget {
  @override
  State<_BingoHome> createState() => _BingoHomeState();
}
```

### ステート
```dart
class _BingoHomeState extends State<_BingoHome> {
  List<Alphabet> _history = [];
  List<Alphabet> _remaining = [...Alphabet.values];
  List<List<Alphabet>> _cells = [];
  final _cellNum = 5;
  late Size _screenSize;
  late Orientation _orientation;
  static const _cellSizeMax = 80.0;
  late double _cellSize;
  late bool _bingo = false;

  @override
  void initState() {
    _cells = _CellGenerator(_cellNum).generate();
  }

  @override
  Widget build(BuildContext context) {
    _screenSize = MediaQuery.of(context).size;
    _orientation = MediaQuery.of(context).orientation;
    switch (_orientation) {
      case Orientation.landscape:
        _cellSize = [
          _screenSize.width / 10,
          _screenSize.height / 7,
          _cellSizeMax
        ].reduce(min);
        break;
      case Orientation.portrait:
        _cellSize = [
          _screenSize.width / 5,
          _screenSize.height / 10,
          _cellSizeMax
        ].reduce(min);
        break;
    }
    return _buildContainer();
  }

  Widget _buildContainer() => Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const SizedBox(height: _cellSizeMax / 2),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () => _pushReset(),
                child: const Text('Reset'),
              ),
              const SizedBox(width: _cellSizeMax / 2),
              ElevatedButton(
                onPressed: () => _pushLottery(),
                child: const Text('Lottery'),
              ),
            ],
          ),
          const SizedBox(height: _cellSizeMax / 2),
          _buildMainContainer(),
        ],
      );

  Widget _buildMainContainer() {
    switch (_orientation) {
      case Orientation.landscape:
        return Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          _buildArea(),
          const SizedBox(width: _cellSizeMax / 2),
          _buildHistory(),
        ]);
      case Orientation.portrait:
        return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          _buildArea(),
          const SizedBox(width: _cellSizeMax / 2),
          _buildHistory(),
        ]);
    }
  }

  Widget _buildArea() => SizedBox(
        width: _cellSize * _cellNum,
        height: _cellSize * _cellNum,
        child: Stack(
          children: [
            _buildColumn(),
            Center(
              child: Container(
                child: _buildInformationText(),
              ),
            ),
          ],
        ),
      );

  Widget? _buildInformationText() {
    if (_bingo) {
      return const Text(
        'BINGO!',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          fontSize: 60.0,
          color: Colors.pink,
        ),
      );
    } else {
      return null;
    }
  }

  Widget _buildColumn() => Column(
        children: List.generate(
          _cellNum,
          (index) => _buildRow(index),
        ),
      );

  Widget _buildRow(int rowIndex) => Row(
        children:
            List.generate(_cellNum, (index) => _buildCell(rowIndex, index)),
      );

  Widget _buildCell(int rowIndex, int columnIndex) {
    final cell = _cells[rowIndex][columnIndex];

    return Container(
      width: _cellSize,
      height: _cellSize,
      decoration: buildBoxDecoration(rowIndex, columnIndex),
      child:
          Center(child: Text(cell.name, style: const TextStyle(fontSize: 18))),
    );
  }

  BoxDecoration buildBoxDecoration(int rowIndex, int columnIndex) {
    if (_history.contains(_cells[rowIndex][columnIndex])) {
      return BoxDecoration(
        border: Border.all(color: Colors.black),
        shape: BoxShape.rectangle,
        color: Colors.blue,
      );
    } else {
      return BoxDecoration(
        border: Border.all(color: Colors.black),
      );
    }
  }

  Widget _buildHistory() {
    switch (_orientation) {
      case Orientation.landscape:
        return Column(children: [
          const Center(child: Text('History', style: TextStyle(fontSize: 18))),
          Container(
            height: _cellSize * 4,
            width: _cellSize * 2,
            decoration: BoxDecoration(
              border: Border.all(color: Colors.black),
            ),
            child: GridView.count(
              crossAxisCount: 4,
              children: List.generate(_history.length,
                  (index) => Center(child: Text(_history[index].name))),
            ),
          )
        ]);
      case Orientation.portrait:
        return Column(children: [
          const Center(child: Text('History', style: TextStyle(fontSize: 18))),
          Container(
            height: _cellSize * 2,
            width: _cellSize * 5,
            decoration: BoxDecoration(
              border: Border.all(color: Colors.black),
            ),
            child: GridView.count(
              crossAxisCount: 10,
              children: List.generate(_history.length,
                  (index) => Center(child: Text(_history[index].name))),
            ),
          )
        ]);
    }
  }

  void _pushReset() {
    setState(() {
      _history = [];
      _remaining = [...Alphabet.values];
      _bingo = false;
      _cells = _CellGenerator(_cellNum).generate();
    });
  }

  void _pushLottery() {
    if (_bingo || _remaining.isEmpty) {
      return;
    }
    setState(() {
      _history.add(_remaining.removeAt(Random().nextInt(_remaining.length)));
      for (var rowIndex = 0; rowIndex < _cellNum; rowIndex++) {
        if (List.generate(_cellNum, (i) => i)
            .every((e) => _history.contains(_cells[rowIndex][e]))) {
          _bingo = true;
          break;
        }
      }
      for (var columnIndex = 0; columnIndex < _cellNum; columnIndex++) {
        if (List.generate(_cellNum, (i) => i)
            .every((e) => _history.contains(_cells[e][columnIndex]))) {
          _bingo = true;
          break;
        }
      }
      if (List.generate(_cellNum, (i) => i)
          .every((e) => _history.contains(_cells[e][e]))) {
        _bingo = true;
      }
      if (List.generate(_cellNum, (i) => i)
          .every((e) => _history.contains(_cells[e][_cellNum - e - 1]))) {
        _bingo = true;
      }
    });
  }
}
```

### セルデータの作成
```dart
class _CellGenerator {
  final int _cellNum;
  final List<Alphabet> _remaining = [...Alphabet.values];

  _CellGenerator(this._cellNum);

  List<List<Alphabet>> generate() {
    var cells = List.generate(
        _cellNum,
        (_) => List.generate(_cellNum,
            (_) => _remaining.removeAt(Random().nextInt(_remaining.length))));
    return cells;
  }
}
```

### セルデータ
```dart
enum Alphabet {
  A,
  B,
  C,
  D,
  E,
  F,
  G,
  H,
  I,
  J,
  K,
  L,
  M,
  N,
  O,
  P,
  Q,
  R,
  S,
  T,
  U,
  V,
  W,
  X,
  Y,
  Z
}

extension on Alphabet {
  String get name => toString().split('.').last;
}
```

## アプリの実行
### アプリを起動
```sh
flutter run
```

### 実行結果
| iOS | web |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/dae6107c-bdfe-6bd5-f257-1e0947ac66b5.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/e7d122c4-ebdd-518a-8077-925f09c54d75.png) |

## GitHub
- https://github.com/iharakenji/bingo