Help us understand the problem. What is going on with this article?

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

はじめに

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

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


実際のアプリの紹介

color_matching.gif

Google Play Store
App Store


説明

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


コードの紹介

全体のコード

全部で4926文字です。


全体のコードを見る
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.
    );
  }
}



解説

アプリの作成

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

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

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


void main()

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

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

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

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

先ほど指定したMyAppを定義しています。
extends StatelessWidgetStatelessWidgetを継承しています。


Widget build()

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


MaterialApp()

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


MyHomePageの定義

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

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

先ほど指定したMyHomePageを定義しています。
extends StatefulWidgetStatefulWidgetを継承しています。
今回はState(状態)を持つためStatefulWidgetを指定しています。


_MyHomePageState createState()

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

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


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


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


State
  int _r = 0;
  int _g = 0;
  int _b = 0;
  bool _showResult;
  List<int> _color;

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


List createColor()
  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()
  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()
  int addColorValue(int value) {
    return value == 255 ? 0 : value + 85;
  }

ユーザーがボタンを押した際に色を足す必要がありますが、255であった場合は0に戻す必要があります。
そのため引数valueで現在の値を受け取り255であった場合は0を、それ以外の場合は85を足しています。


void clearState()
  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()
  @override
  void initState() {
    super.initState();
    clearState();
  }

Widgetの作成時に呼ばれる関数です。
先ほど記載したclearState()を呼ぶことで状態を初期化しています。


Widget build()

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


Scaffold
    return Scaffold(
      appBar: AppBar(
        title: Text('Color Matching Game'),
      ),
      body: 次で解説,
    );

レイアウトの土台となるWidgetです。
appBarAppBar()を指定できます。今回はタイトルを指定しています。
bodyには画面のメインコンテンツを指定します。次で解説します。


body
      body: Center(
          child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ... 次で解説  
        ],
      )),

Center()以下のWidgetが画面の中央に配置されます。
Column()を指定することでWidgetを縦並びに配置します。
mainAxisSizeでColumn Widgetのサイズを必要なだけ最小と指定しています。
縦並びに配置するWidgetたちはchildrenに指定します。(次で解説)


children1(お題の色の表示)
          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()は箱です。高さと幅を同値にして正方形を作成し、decorationshape: BoxShape.circleを指定することで正円にしています。
ヒントの最大値の色を表示するために"The biggest one is ${maxColorLabel()}"を指定しています。
色は生成したお題の色と同じ色にするためにcolor: Color.fromRGBO(_color[0], _color[1], _color[2], 1.0)を指定しています。


children2(ボタンの表示)
          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で文字色を指定しています。
childText()を指定することでボタン上に文字を表示しています。


children3(結果の表示)
          _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に参加した + アプリの作り方

実際のRepositoryは以下リンクから。
Github Repository

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


  1. 色の3原色(Red, Green, Blue)の略です。 

  2. Flutterのwidget一覧 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした