はじめに
今回は、Flutterと実際のコードを紹介します。
実際にGoogle主催のFlutterCreateという大会に提出したアプリの提出時のコードの解説をします。
5000文字以内で作成した簡単なアプリですが、ストアリリースもしています。
実際のアプリの紹介
説明
お題の色が提示されるため、RGB1をそれぞれ3段階で設定をしてお題の色を作るという色合わせゲームです。
RGBの最大値がヒントとして表示されます。(複数の場合はMultipleと表示)
コードの紹介
全体のコード
全部で4926文字です。
全体のコードを見る
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 run
でlib/main.dart
のmain()
が呼ばれます。
ここで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 StatelessWidget
でStatelessWidget
を継承しています。
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 StatefulWidget
でStatefulWidget
を継承しています。
今回は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です。
appBar
でAppBar()
を指定できます。今回はタイトルを指定しています。
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()
は箱です。高さと幅を同値にして正方形を作成し、decoration
でshape: 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
で文字色を指定しています。
child
にText()
を指定することでボタン上に文字を表示しています。
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
質問やアドバイスがありましたらコメントしてください。
-
色の3原色(Red, Green, Blue)の略です。 ↩