Flutterで5000文字以内でアプリを作る（＋Google主催の大会に提出した）

# はじめに
今回は、Flutterと実際のコードを紹介します。

実際にGoogle主催の[FlutterCreate](https://flutter.dev/create)という大会に提出したアプリの提出時のコードの解説をします。
5000文字以内で作成した簡単なアプリですが、ストアリリースもしています。

---

# 実際のアプリの紹介
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222499/46cf04d7-d554-b6f6-b5fe-eb1a8efbc2ac.gif" alt="color_matching.gif" width=50%> 

[Google Play Store](https://play.google.com/store/apps/details?id=com.naro143.color_matching_game)
[App Store](https://apps.apple.com/us/app/color-matching-game-flutter/id1457916350)

---

## 説明
お題の色が提示されるため、RGB[^1]をそれぞれ3段階で設定をしてお題の色を作るという色合わせゲームです。
RGBの最大値がヒントとして表示されます。（複数の場合はMultipleと表示）

---

# コードの紹介
## 全体のコード
全部で**4926文字**です。

<details>
  <summary>全体のコードを見る</summary>
<div>

```dart:main.dart
import 'dart:math';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Color matching game',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _r = 0;
  int _g = 0;
  int _b = 0;
  bool _showResult;
  List<int> _color;

  List<int> createColor() {
    return List.generate(3, (i) => 85 * Random().nextInt(3));
  }

  String maxColorLabel() {
    List colorLabel = ["Red", "Green", "Blue", "Multiple"];
    int maxValue = _color.reduce((curr, next) => max(curr, next));
    if (_color.where((value) => maxValue == value).length > 1) {
      return colorLabel[3];
    }
    return colorLabel[_color.indexWhere((value) => maxValue == value)];
  }

  int addColorValue(int value) {
    return value == 255 ? 0 : value + 85;
  }

  void clearState() {
    setState(() {
      _color = createColor();
      _r = 0;
      _g = 0;
      _b = 0;
      _showResult = false;
    });
  }

  @override
  void initState() {
    super.initState();
    clearState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Color Matching Game'),
      ),
      body: Center(
          child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text(
            "Theme color",
          ),
          SizedBox(height: 10.0),
          Container(
            height: 100.0,
            width: 100.0,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0),
            ),
          ),
          SizedBox(height: 10.0),
          Text(
            "The biggest one is ${maxColorLabel()}",
            style: TextStyle(
                color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0)),
          ),
          SizedBox(height: 10.0),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _r = addColorValue(_r);
                  });
                },
                color: Color.fromRGBO(_r, 0, 0, 1),
                textColor: Colors.white,
                child: Text(
                  "RED",
                ),
              ),
              SizedBox(width: 10.0),
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _g = addColorValue(_g);
                  });
                },
                color: Color.fromRGBO(0, _g, 0, 1),
                textColor: Colors.white,
                child: Text(
                  "GREEN",
                ),
              ),
              SizedBox(width: 10.0),
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _b = addColorValue(_b);
                  });
                },
                color: Color.fromRGBO(0, 0, _b, 1),
                textColor: Colors.white,
                child: Text(
                  "BLUE",
                ),
              ),
            ],
          ),
          SizedBox(height: 10.0),
          _showResult
              ? Column(
                  children: <Widget>[
                    Text(
                      "Your color",
                    ),
                    SizedBox(height: 10.0),
                    Container(
                      height: 100.0,
                      width: 100.0,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Color.fromRGBO(_r, _g, _b, 1.0),
                      ),
                    ),
                    SizedBox(height: 10.0),
                    Text(
                      _r == _color[0] && _g == _color[1] && _b == _color[2]
                          ? "Match!"
                          : "Not Match",
                      style: TextStyle(color: Color.fromRGBO(_r, _g, _b, 1.0)),
                    ),
                  ],
                )
              : Container(),
          SizedBox(height: 10.0),
          MaterialButton(
            onPressed: () {
              if (_showResult) {
                clearState();
              } else {
                setState(() {
                  _showResult = true;
                });
              }
            },
            child: Text(
              _showResult ? "NEXT COLOR" : "SHOW RESULT",
              style: TextStyle(color: Colors.blue),
            ),
          ),
        ],
      )), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```
</div>
</details>



---

### 解説
#### アプリの作成
```dart
import 'dart:math';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());
```

必要なパッケージ（dart:mathとflutter）をimportしてます。
FlutterはWidget[^2]というまとまった単位を作成して、Widgetの組み合わせでアプリを作っていきます。
**Flutterで提供されるWidgetを組み合わせて自作Widgetを作り、自作Widgetを組み合わせてアプリを作ると認識していただければいいです。**

---

##### void main()
デフォルトの設定では`flutter run`で`lib/main.dart`の`main()`が呼ばれます。
ここで`MyApp`インスタンス(Widget)を`runApp()`に渡しています。
**アプリの土台となるWidgetを指定していると認識していただければいいです。**

`() =>`という書き方は`() {}`と同じ意味です。
以下の書き方でも同じ動作をします。

```dart
void main() {
  runApp(MyApp());
}
```

---

#### MyAppの定義
```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Color matching game',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}
```
先ほど指定した`MyApp`を定義しています。
`extends　StatelessWidget`で`StatelessWidget`を継承しています。

---

##### Widget build()
ここ`build()`で自作Widget(今回は`MyApp`)の構築に必要なWidgetを組み上げます。
組み上げたものは`return`で指定します。

---

##### MaterialApp()
マテリアルデザインのアプリの土台となるWidgetです。
`debugShowCheckedModeBanner `でデバッグモードであるとわかるバナーを表示するかを指定できます。
`title`でアプリのタイトルを指定できます。
`theme`でアプリのテーマを指定します。今回は`primarySwatch`を指定することでアプリのカラーパレットをしています。（青を基調としたアプリと指定していると思ってください）
`home`でアプリ起動直後に表示されるWidgetを指定しています。

---

#### MyHomePageの定義
```dart
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}
```
先ほど指定した`MyHomePage`を定義しています。
`extends　StatefulWidget`で`StatefulWidget`を継承しています。
今回はState（状態）を持つため`StatefulWidget`を指定しています。

---

##### _MyHomePageState createState()
先ほど状態を持つと記載しました。
ここ`createState()`でState(今回は`_MyHomePageState()`)を指定します。

**状態を持つWidgetを定義して、状態を指定していると認識していただければいいです。**

---

#### _MyHomePageStateの定義
<details>
  <summary>コードを見る</summary>
  <div>

```dart
class _MyHomePageState extends State<MyHomePage> {
  int _r = 0;
  int _g = 0;
  int _b = 0;
  bool _showResult;
  List<int> _color;

  List<int> createColor() {
    return List.generate(3, (i) => 85 * Random().nextInt(3));
  }

  String maxColorLabel() {
    List colorLabel = ["Red", "Green", "Blue", "Multiple"];
    int maxValue = _color.reduce((curr, next) => max(curr, next));
    if (_color.where((value) => maxValue == value).length > 1) {
      return colorLabel[3];
    }
    return colorLabel[_color.indexWhere((value) => maxValue == value)];
  }

  int addColorValue(int value) {
    return value == 255 ? 0 : value + 85;
  }

  void clearState() {
    setState(() {
      _color = createColor();
      _r = 0;
      _g = 0;
      _b = 0;
      _showResult = false;
    });
  }

  @override
  void initState() {
    super.initState();
    clearState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Color Matching Game'),
      ),
      body: Center(
          child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text(
            "Theme color",
          ),
          SizedBox(height: 10.0),
          Container(
            height: 100.0,
            width: 100.0,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0),
            ),
          ),
          SizedBox(height: 10.0),
          Text(
            "The biggest one is ${maxColorLabel()}",
            style: TextStyle(
                color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0)),
          ),
          SizedBox(height: 10.0),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _r = addColorValue(_r);
                  });
                },
                color: Color.fromRGBO(_r, 0, 0, 1),
                textColor: Colors.white,
                child: Text(
                  "RED",
                ),
              ),
              SizedBox(width: 10.0),
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _g = addColorValue(_g);
                  });
                },
                color: Color.fromRGBO(0, _g, 0, 1),
                textColor: Colors.white,
                child: Text(
                  "GREEN",
                ),
              ),
              SizedBox(width: 10.0),
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _b = addColorValue(_b);
                  });
                },
                color: Color.fromRGBO(0, 0, _b, 1),
                textColor: Colors.white,
                child: Text(
                  "BLUE",
                ),
              ),
            ],
          ),
          SizedBox(height: 10.0),
          _showResult
              ? Column(
                  children: <Widget>[
                    Text(
                      "Your color",
                    ),
                    SizedBox(height: 10.0),
                    Container(
                      height: 100.0,
                      width: 100.0,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Color.fromRGBO(_r, _g, _b, 1.0),
                      ),
                    ),
                    SizedBox(height: 10.0),
                    Text(
                      _r == _color[0] && _g == _color[1] && _b == _color[2]
                          ? "Match!"
                          : "Not Match",
                      style: TextStyle(color: Color.fromRGBO(_r, _g, _b, 1.0)),
                    ),
                  ],
                )
              : Container(),
          SizedBox(height: 10.0),
          MaterialButton(
            onPressed: () {
              if (_showResult){
                clearState();
              } else {
                setState(() {
                  _showResult = true;
                });
              }
            },
            child: Text(
              _showResult ? "NEXT COLOR" : "SHOW RESULT",
              style: TextStyle(color: Colors.blue),
            ),
          ),
        ],
      )),
    );
  }
}
```
  </div>
</details>


先ほど指定した状態`_MyHomePageState`を定義しています。
このアプリの機能の大部分が集約されています。

---

##### State
```dart
  int _r = 0;
  int _g = 0;
  int _b = 0;
  bool _showResult;
  List<int> _color;
```
今回のStateです。
`_r`はユーザーが入力した赤色の数値を管理します。
`_g`はユーザーが入力した緑色の数値を管理します。
`_b`はユーザーが入力した青色の数値を管理します。
`_showResult`はユーザーが入力した結果が表示されているかを管理します。
`_color`はRGBの各値をListで管理します。

---

##### List<int> createColor()
```dart
  List<int> createColor() {
    return List.generate(3, (i) => 85 * Random().nextInt(3));
  }
```
お題の色RGBを作成する関数です。
今回RGBの各値の段階は、[0, 85, 170, 255]の4段階のため、85に0~3のランダムな値を乗算します。
RGBなので3回行い要素数3のリストとしてます。（例: [0, 255, 170]）

---

##### String maxColorLabel()
```dart
  String maxColorLabel() {
    List colorLabel = ["Red", "Green", "Blue", "Multiple"];
    int maxValue = _color.reduce((curr, next) => max(curr, next));
    if (_color.where((value) => maxValue == value).length > 1) {
      return colorLabel[3];
    }
    return colorLabel[_color.indexWhere((value) => maxValue == value)];
  }
```
ヒントとなるRGBの最大の色を返す関数です。
先ほど生成したRGBのリストから最大値を取得し`maxValue`に代入しています。
RGBのリストに対して最大値で検索を行い結果が複数であれば`"Multiple"`を返します。
複数ない場合は最大値の色を返します。

---

##### addColorValue()
```dart
  int addColorValue(int value) {
    return value == 255 ? 0 : value + 85;
  }
```
ユーザーがボタンを押した際に色を足す必要がありますが、255であった場合は0に戻す必要があります。
そのため引数`value`で現在の値を受け取り255であった場合は0を、それ以外の場合は85を足しています。

---

##### void clearState()
```dart
  void clearState() {
    setState(() {
      _color = createColor();
      _r = 0;
      _g = 0;
      _b = 0;
      _showResult = false;
    });
  }
```
状態(State)を初期化する関数です。
`setState()`で指定した状態を元に画面を更新します。
`_color`には先ほど記載した`createColor()`でお題の色を再生成して代入しています。
`_r`, `_g`, `_b`には0を代入しています。
`_showResult`にはfalseを代入しています。

---

##### void initState()
```dart
  @override
  void initState() {
    super.initState();
    clearState();
  }
```
Widgetの作成時に呼ばれる関数です。
先ほど記載した`clearState()`を呼ぶことで状態を初期化しています。

---

##### Widget build()
ここ`build()`で自作Widget(今回は`_MyHomePageState`)の構築に必要なWidgetを組み上げます。
組み上げたものはreturnで指定します。
大きいため分割して解説します。

---

###### Scaffold
```dart
    return Scaffold(
      appBar: AppBar(
        title: Text('Color Matching Game'),
      ),
      body: 次で解説,
    );
```
レイアウトの土台となるWidgetです。
`appBar`で`AppBar()`を指定できます。今回はタイトルを指定しています。
`body`には画面のメインコンテンツを指定します。次で解説します。

---

###### body
```dart
      body: Center(
          child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ... 次で解説  
        ],
      )),
```
`Center()`以下のWidgetが画面の中央に配置されます。
`Column()`を指定することでWidgetを縦並びに配置します。
`mainAxisSize`でColumn Widgetのサイズを必要なだけ最小と指定しています。
縦並びに配置するWidgetたちは`children`に指定します。（次で解説）

---

###### children1(お題の色の表示)
```dart
          Text(
            "Theme color",
          ),
          SizedBox(height: 10.0),
          Container(
            height: 100.0,
            width: 100.0,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0),
            ),
          ),
          SizedBox(height: 10.0),
          Text(
            "The biggest one is ${maxColorLabel()}",
            style: TextStyle(
                color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0)),
          ),
          SizedBox(height: 10.0),
```
`Text()`で文字を表示します。
`SizedBox(height: 10.0)`は隙間を取っていると認識していただければいいです。
`Container()`は箱です。高さと幅を同値にして正方形を作成し、`decoration`で`shape: BoxShape.circle`を指定することで正円にしています。
ヒントの最大値の色を表示するために`"The biggest one is ${maxColorLabel()}"`を指定しています。
色は生成したお題の色と同じ色にするために`color: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0)`を指定しています。

---

###### children2(ボタンの表示)
```dart
          Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _r = addColorValue(_r);
                  });
                },
                color: Color.fromRGBO(_r, 0, 0, 1),
                textColor: Colors.white,
                child: Text(
                  "RED",
                ),
              ),
              SizedBox(width: 10.0),
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _g = addColorValue(_g);
                  });
                },
                color: Color.fromRGBO(0, _g, 0, 1),
                textColor: Colors.white,
                child: Text(
                  "GREEN",
                ),
              ),
              SizedBox(width: 10.0),
              MaterialButton(
                onPressed: () {
                  setState(() {
                    _b = addColorValue(_b);
                  });
                },
                color: Color.fromRGBO(0, 0, _b, 1),
                textColor: Colors.white,
                child: Text(
                  "BLUE",
                ),
              ),
            ],
          ),
          SizedBox(height: 10.0),
```
`Row()`でWidgetを横並びに配置します。
`Column()`の時と同じくRow Widgetのサイズを必要なだけ最小と指定しています。
横並びに配置するWidgetたちは`children`に指定します。
`MaterialButton()`でボタンを生成しています。
`onPressed`は押された時の挙動を指定できます。今回は押された色(`_r`, `_g`, `_b`)の値を変更するよう指定しています。
`color`でボタンの色を`textColor`で文字色を指定しています。
`child`に`Text()`を指定することでボタン上に文字を表示しています。

---

###### children3(結果の表示)
```dart
          _showResult
              ? Column(
                  children: <Widget>[
                    Text(
                      "Your color",
                    ),
                    SizedBox(height: 10.0),
                    Container(
                      height: 100.0,
                      width: 100.0,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Color.fromRGBO(_r, _g, _b, 1.0),
                      ),
                    ),
                    SizedBox(height: 10.0),
                    Text(
                      _r == _color[0] && _g == _color[1] && _b == _color[2]
                          ? "Match!"
                          : "Not Match",
                      style: TextStyle(color: Color.fromRGBO(_r, _g, _b, 1.0)),
                    ),
                  ],
                )
              : Container(),
          SizedBox(height: 10.0),
          MaterialButton(
            onPressed: () {
              if (_showResult) {
                clearState();
              } else {
                setState(() {
                  _showResult = true;
                });
              }
            },
            child: Text(
              _showResult ? "NEXT COLOR" : "SHOW RESULT",
              style: TextStyle(color: Colors.blue),
            ),
          ),
```
`_showResult`がtrueの時に結果を表示します。falseの時は空の`Container()`を指定しています。
正円の表示はお題の色と同じです。
`_r == _color[0] && _g == _color[1] && _b == _color[2]`でお題の色とユーザーの入力した各値が同じかを判定しています。
最後のボタンは`_showResult`がtrueなら状態を初期化して新しいお題を表示します。
`_showResult`がfalseの時はtrueに変更することで結果を表示します。

---

# 最後に
ここまで読んでくださりありがとうございます。
この記事では実際のコードを解説しました。

アプリのアイデアを思いつくまでの思考は以下の記事にて執筆しています。
[Google主催の世界大会、FlutterCreateに参加した ＋ アプリの作り方](https://note.mu/naro143/n/neff048564b04)

実際のRepositoryは以下リンクから。
[Github Repository](https://github.com/naro143/color_matching_game)

質問やアドバイスがありましたらコメントしてください。

[^1]: 色の3原色（Red, Green, Blue）の略です。
[^2]: [Flutterのwidget一覧](https://api.flutter.dev/flutter/widgets/widgets-library.html)