はじめに
以下の記事を読んで衝撃的だったので、私もFlutterで計算機アプリを作成してみました。
@yanagih さん、いつも素晴らしい記事をありがとうございます!Open Interpreter の記事最高です!
概要
iPhoneの計算機アプリのキャプチャ画像をChatGPTにアップロードして、色々依頼したら、10分ぐらいでFlutterの計算機アプリが出来ちゃった。
(Input) 計算機のキャプチャ | (Output) 完成したFlutterアプリ |
---|---|
10分ぐらいでこの品質のアプリが出来て、尚且つテストコードまで出せるなんて神。
もう少しレイアウト調整したかったのですが、最後はChatGPTのネットワークエラーが頻発したので、ここまでにしておきました。
備考
途中経過のキャプチャ一覧
STEP2 | STEP3 | STEP4 | STEP5 | STEP6 |
---|---|---|---|---|
↑の表は以下のブックマークレットで作成
FlutterのソースコードはDartPadですぐに実行できます。
早速計算機アプリを作ってみる
STEP1: 「あなたは、Flutterのスペシャリストです。この電卓プログラムを再現してください。」
依頼内容
ChatGPTに計算機アプリのキャプチャをアップロードしてから、
あなたは、Flutterのスペシャリストです。この電卓プログラムを再現してください。
出てきたソース
import 'package:flutter/material.dart';
void main() => runApp(CalculatorApp());
class CalculatorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Calculator(),
);
}
}
class Calculator extends StatefulWidget {
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
String display = "0";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Calculator')),
body: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.bottomRight,
padding: EdgeInsets.all(32),
child: Text(display, style: TextStyle(fontSize: 36)),
),
),
for (var row in [
['7', '8', '9', '×'],
['4', '5', '6', '−'],
['1', '2', '3', '+'],
['AC', '0', '=', '/']
])
Row(
children: row.map((btn) => buildButton(btn)).toList(),
)
],
),
);
}
Widget buildButton(String title) {
return Expanded(
child: FlatButton(
color: Colors.orange,
onPressed: () => onButtonPressed(title),
child: Text(title, style: TextStyle(fontSize: 24, color: Colors.white)),
),
);
}
void onButtonPressed(String value) {
setState(() {
if (value == 'AC') {
display = '0';
} else if ('0123456789'.contains(value)) {
if (display == '0') {
display = value;
} else {
display += value;
}
} else {
// ここで計算ロジックを追加することができます。
// 今回のサンプルでは省略しています。
}
});
}
}
メッセージ
このコードは、基本的なボタンのレイアウトと表示を持つシンプルな電卓のUIをFlutterで作成しています。しかし、実際の計算ロジック(加算、減算、乗算など)は省略されています。このロジックを実装するには、onButtonPressed関数を編集する必要があります。
このコードを使用してFlutterで電卓アプリケーションを作成する際の参考としてください。
DartPadで実行したら、エラーが出たので、エラー内容をそのまま貼り付けてみる
STEP2: 「The method 'FlatButton' isn't defined for the type '_CalculatorState'.」
出てきたソース
Widget buildButton(String title) {
return Expanded(
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.orange,
primary: Colors.white,
padding: EdgeInsets.all(16),
),
onPressed: () => onButtonPressed(title),
child: Text(title, style: TextStyle(fontSize: 24)),
),
);
}
おー、それっぽいのが出てきた。けど、計算ロジックはまだ入ってないので依頼してみる
STEP3: 「実際の計算ロジック(加算、減算、乗算など)も追加してください」
出てきたソース
import 'package:flutter/material.dart';
void main() => runApp(CalculatorApp());
class CalculatorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Calculator(),
);
}
}
class Calculator extends StatefulWidget {
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
String display = "0";
String? operation;
double? firstValue;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Calculator')),
body: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.bottomRight,
padding: EdgeInsets.all(32),
child: Text(display, style: TextStyle(fontSize: 36)),
),
),
for (var row in [
['7', '8', '9', '×'],
['4', '5', '6', '−'],
['1', '2', '3', '+'],
['AC', '0', '=', '/']
])
Row(
children: row.map((btn) => buildButton(btn)).toList(),
)
],
),
);
}
Widget buildButton(String title) {
return Expanded(
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.orange,
primary: Colors.white,
padding: EdgeInsets.all(16),
),
onPressed: () => onButtonPressed(title),
child: Text(title, style: TextStyle(fontSize: 24)),
),
);
}
void onButtonPressed(String value) {
setState(() {
if ('0123456789'.contains(value)) {
if (display == '0' || (operation != null && firstValue == null)) {
display = value;
} else {
display += value;
}
} else if (value == 'AC') {
display = '0';
operation = null;
firstValue = null;
} else if (['+', '−', '×', '/'].contains(value)) {
operation = value;
firstValue = double.parse(display);
display = '0';
} else if (value == '=') {
if (operation != null && firstValue != null) {
switch (operation) {
case '+':
display = (firstValue! + double.parse(display)).toString();
break;
case '−':
display = (firstValue! - double.parse(display)).toString();
break;
case '×':
display = (firstValue! * double.parse(display)).toString();
break;
case '/':
if (double.parse(display) == 0.0) {
display = "Error";
} else {
display = (firstValue! / double.parse(display)).toStringAsFixed(2); // 小数点以下2桁で表示
}
break;
}
firstValue = null;
operation = null;
}
}
});
}
}
おー、それっぽく動いてる!が、小数点ボタンがないので追加依頼+その他レイアウト調整もお願いしてみる。
STEP4: レイアウト調整
何度かやり取りをした結果、以下のソースになった
import 'package:flutter/material.dart';
void main() => runApp(CalculatorApp());
class CalculatorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Calculator(),
);
}
}
class Calculator extends StatefulWidget {
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
String display = "0";
String? operation;
double? firstValue;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: Container(alignment: Alignment.bottomRight, padding: EdgeInsets.all(30.0),
child: Text(display, style: TextStyle(fontSize: 70, color: Colors.white)))),
buildButtonRow(['7', '8', '9', '/']),
buildButtonRow(['4', '5', '6', '×']),
buildButtonRow(['1', '2', '3', '−']),
buildButtonRow(['AC', '0', '.', '+']),
buildButtonRow(['', '', '', '=']),
],
);
}
Widget buildButtonRow(List<String> titles) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: titles.map((title) => buildButton(title)).toList(),
);
}
Widget buildButton(String title) {
Color buttonColor;
Color textColor = Colors.white; // デフォルトのテキストの色
if (['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'].contains(title)) {
buttonColor = Colors.grey[800]!;
} else if (['/', '×', '−', '+', '='].contains(title)) {
buttonColor = Colors.orange;
} else {
buttonColor = Colors.grey[300]!;
textColor = Colors.black;
}
return title.isEmpty ? Container(width: 70, height: 70) : Container(
width: 70,
height: 70,
margin: EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => onButtonPressed(title),
child: Text(title, style: TextStyle(fontSize: 24, color: textColor)),
style: ElevatedButton.styleFrom(
primary: buttonColor,
onPrimary: Colors.white,
shape: CircleBorder(),
),
),
);
}
void onButtonPressed(String value) {
setState(() {
if ('0123456789'.contains(value)) {
if (display == '0' || (operation != null && firstValue == null)) {
display = value;
} else {
display += value;
}
} else if (value == 'AC') {
display = '0';
operation = null;
firstValue = null;
} else if (['+', '−', '×', '/'].contains(value)) {
operation = value;
firstValue = double.parse(display);
display = '0';
} else if (value == '=') {
if (operation != null && firstValue != null) {
switch (operation) {
case '+':
display = (firstValue! + double.parse(display)).toString();
break;
case '−':
display = (firstValue! - double.parse(display)).toString();
break;
case '×':
display = (firstValue! * double.parse(display)).toString();
break;
case '/':
if (double.parse(display) == 0.0) {
display = "Error";
} else {
display = (firstValue! / double.parse(display)).toStringAsFixed(2); // 小数点以下2桁で表示
}
break;
}
firstValue = null;
operation = null;
}
} else if (value == '.' && !display.contains('.')) {
display += '.';
}
});
}
}
おー、これはいい感じになった!だけど、ソースがやっつけ感満載なので、リファクタリングを依頼してみる。
STEP5: リファクタリング
依頼内容
OKです!それでは、そのソースを以下を意識してリファクタリングして下さい
- あなたは開発歴10年以上のベテランプログラマーです。
- 拡張性、変更容易性、テスト容易性を重視して、設計を見直して下さい。
- 解説は不要なので、ソースコードだけ全文ください。
出てきたソース
import 'package:flutter/material.dart';
void main() => runApp(CalculatorApp());
class CalculatorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(backgroundColor: Colors.black, body: Calculator()),
);
}
}
class Calculator extends StatefulWidget {
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
String display = "0";
String? operation;
double? firstValue;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(child: Container(alignment: Alignment.bottomRight, padding: EdgeInsets.all(16.0), child: Text(display, style: TextStyle(fontSize: 32, color: Colors.white)))),
for (var row in _buttonRows) buildButtonRow(row),
],
);
}
final _buttonRows = [
['7', '8', '9', '/'],
['4', '5', '6', '×'],
['1', '2', '3', '−'],
['AC', '0', '.', '+'],
['', '', '', '=']
];
Widget buildButtonRow(List<String> titles) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: titles.map((title) => CalculatorButton(title: title, onPressed: onButtonPressed)).toList(),
);
}
void onButtonPressed(String value) {
setState(() {
if ('0123456789'.contains(value)) {
if (display == '0' || (operation != null && firstValue == null)) {
display = value;
} else {
display += value;
}
} else if (value == 'AC') {
display = '0';
operation = null;
firstValue = null;
} else if (['+', '−', '×', '/'].contains(value)) {
operation = value;
firstValue = double.parse(display);
display = '0';
} else if (value == '=') {
if (operation != null && firstValue != null) {
switch (operation) {
case '+':
display = (firstValue! + double.parse(display)).toString();
break;
case '−':
display = (firstValue! - double.parse(display)).toString();
break;
case '×':
display = (firstValue! * double.parse(display)).toString();
break;
case '/':
if (double.parse(display) == 0.0) {
display = "Error";
} else {
display = (firstValue! / double.parse(display)).toStringAsFixed(2); // 小数点以下2桁で表示
}
break;
}
firstValue = null;
operation = null;
}
} else if (value == '.' && !display.contains('.')) {
display += '.';
}
});
}
}
class CalculatorButton extends StatelessWidget {
final String title;
final Function(String) onPressed;
const CalculatorButton({required this.title, required this.onPressed});
@override
Widget build(BuildContext context) {
Color buttonColor = _buttonColors.containsKey(title) ? _buttonColors[title]! : Colors.grey[300]!;
Color textColor = (title == 'AC') ? Colors.black : Colors.white;
return title.isEmpty ? _emptySpace() : _button(buttonColor, textColor);
}
Widget _emptySpace() => Container(width: 70, height: 70);
Widget _button(Color color, Color textColor) {
return Container(
width: 70,
height: 70,
margin: EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => onPressed(title),
child: Text(title, style: TextStyle(fontSize: 24, color: textColor)),
style: ElevatedButton.styleFrom(
primary: color,
shape: CircleBorder(),
),
),
);
}
static const _buttonColors = {
'0': Colors.grey,
'1': Colors.grey,
'2': Colors.grey,
'3': Colors.grey,
'4': Colors.grey,
'5': Colors.grey,
'6': Colors.grey,
'7': Colors.grey,
'8': Colors.grey,
'9': Colors.grey,
'.': Colors.grey,
'/': Colors.orange,
'×': Colors.orange,
'−': Colors.orange,
'+': Colors.orange,
'=': Colors.orange,
};
}
ちょっと、ちょっと、ちょっと。細かいことは抜きにして、これはかなり良い感じになった!
1分ぐらいで対応したリファクタリングとは思えないレベル。
あとはリファクタリングでボタンの色が変わってしまったので、もう少し調整してもらう。
STEP6: ボタンの色が変わってしまったので、もう少し調整
import 'package:flutter/material.dart';
void main() => runApp(CalculatorApp());
class CalculatorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(backgroundColor: Colors.black, body: Calculator()),
);
}
}
class Calculator extends StatefulWidget {
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
String display = "0";
String? operation;
double? firstValue;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(child: Container(alignment: Alignment.bottomRight, padding: EdgeInsets.all(16.0), child: Text(display, style: TextStyle(fontSize: 70, color: Colors.white)))),
for (var row in _buttonRows) buildButtonRow(row),
],
);
}
final _buttonRows = [
['7', '8', '9', '/'],
['4', '5', '6', '×'],
['1', '2', '3', '−'],
['AC', '0', '.', '+'],
['', '', '', '=']
];
Widget buildButtonRow(List<String> titles) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: titles.map((title) => CalculatorButton(title: title, onPressed: onButtonPressed)).toList(),
);
}
void onButtonPressed(String value) {
setState(() {
if ('0123456789'.contains(value)) {
if (display == '0' || (operation != null && firstValue == null)) {
display = value;
} else {
display += value;
}
} else if (value == 'AC') {
display = '0';
operation = null;
firstValue = null;
} else if (['+', '−', '×', '/'].contains(value)) {
operation = value;
firstValue = double.parse(display);
display = '0';
} else if (value == '=') {
if (operation != null && firstValue != null) {
switch (operation) {
case '+':
display = (firstValue! + double.parse(display)).toString();
break;
case '−':
display = (firstValue! - double.parse(display)).toString();
break;
case '×':
display = (firstValue! * double.parse(display)).toString();
break;
case '/':
if (double.parse(display) == 0.0) {
display = "Error";
} else {
display = (firstValue! / double.parse(display)).toStringAsFixed(2); // 小数点以下2桁で表示
}
break;
}
firstValue = null;
operation = null;
}
} else if (value == '.' && !display.contains('.')) {
display += '.';
}
});
}
}
class CalculatorButton extends StatelessWidget {
final String title;
final Function(String) onPressed;
const CalculatorButton({required this.title, required this.onPressed});
@override
Widget build(BuildContext context) {
Color buttonColor = _buttonColors.containsKey(title) ? _buttonColors[title]! : Colors.grey;
Color textColor = (title == 'AC') ? Colors.black : Colors.white;
return title.isEmpty ? _emptySpace() : _button(buttonColor, textColor);
}
Widget _emptySpace() => Container(width: 70, height: 70);
Widget _button(Color color, Color textColor) {
return Container(
width: 70,
height: 70,
margin: EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => onPressed(title),
child: Text(title, style: TextStyle(fontSize: 24, color: textColor)),
style: ElevatedButton.styleFrom(
primary: color,
shape: CircleBorder(),
),
),
);
}
static const _buttonColors = {
'0': Colors.white24,
'1': Colors.white24,
'2': Colors.white24,
'3': Colors.white24,
'4': Colors.white24,
'5': Colors.white24,
'6': Colors.white24,
'7': Colors.white24,
'8': Colors.white24,
'9': Colors.white24,
'.': Colors.white24,
'/': Colors.orange,
'×': Colors.orange,
'−': Colors.orange,
'+': Colors.orange,
'=': Colors.orange,
};
}
うわ、10分ぐらいでこの完成度は凄すぎる!
本当ならレイアウトをもう少し調整してもらいたいところだけど、ChatGPTがネットワークエラー頻発してるので、レイアウト調整はこれで終了とする。
残りのレイアウト調整はプロンプトで指示を続けて、他にもバグがあればそのまま伝えれば、いずれ完成すると思うし。
最後にテストコードを出してもらい、今回は終わりとする。
STEP7: テストコード
依頼内容
素晴らしい!テストコードもお願いします!
出てきたソース
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'your_main_file_name.dart'; // 上のソースコードが保存されているファイル名に置き換えてください。
void main() {
testWidgets('Calculator display starts with 0', (WidgetTester tester) async {
// アプリをビルド
await tester.pumpWidget(CalculatorApp());
// displayが0で始まるか確認
expect(find.text('0'), findsOneWidget);
});
testWidgets('Pressing number buttons updates the display', (WidgetTester tester) async {
await tester.pumpWidget(CalculatorApp());
// '1' ボタンをタップ
await tester.tap(find.text('1'));
await tester.pump();
expect(find.text('1'), findsOneWidget);
// '2' ボタンをタップ
await tester.tap(find.text('2'));
await tester.pump();
expect(find.text('12'), findsOneWidget);
});
testWidgets('Pressing AC resets the display', (WidgetTester tester) async {
await tester.pumpWidget(CalculatorApp());
// 数字を入力
await tester.tap(find.text('9'));
await tester.pump();
expect(find.text('9'), findsOneWidget);
// 'AC' をタップしてリセット
await tester.tap(find.text('AC'));
await tester.pump();
expect(find.text('0'), findsOneWidget);
});
// 他の計算ロジックやボタンの動作に関するテストも追加できます。
}
注意:テストコードの動作確認はしてないです。
さいごに
マジ凄いっす。これは本当に凄い。
あまり動作確認はしてないので、バグはあると思いますが、許して下さい。