8
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GPT-4VでFlutterの計算機アプリを依頼したら凄かった

Posted at

はじめに

以下の記事を読んで衝撃的だったので、私もFlutterで計算機アプリを作成してみました。

@yanagih さん、いつも素晴らしい記事をありがとうございます!Open Interpreter の記事最高です!

概要

iPhoneの計算機アプリのキャプチャ画像をChatGPTにアップロードして、色々依頼したら、10分ぐらいでFlutterの計算機アプリが出来ちゃった。

(Input) 計算機のキャプチャ (Output) 完成したFlutterアプリ
iPhone_計算機.jpeg STEP6.png

10分ぐらいでこの品質のアプリが出来て、尚且つテストコードまで出せるなんて神。
もう少しレイアウト調整したかったのですが、最後はChatGPTのネットワークエラーが頻発したので、ここまでにしておきました。

備考

途中経過のキャプチャ一覧

STEP2 STEP3 STEP4 STEP5 STEP6
STEP2.png STEP3.png STEP4.png STEP5.png STEP6.png

↑の表は以下のブックマークレットで作成

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

DartPadで実行
STEP2.png

おー、それっぽいのが出てきた。けど、計算ロジックはまだ入ってないので依頼してみる

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

DartPadで実行
STEP3.png

おー、それっぽく動いてる!が、小数点ボタンがないので追加依頼+その他レイアウト調整もお願いしてみる。

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 += '.';
      }
    });
  }
}

DartPadで実行
STEP4.png

おー、これはいい感じになった!だけど、ソースがやっつけ感満載なので、リファクタリングを依頼してみる。

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

DartPadで実行
STEP5.png

ちょっと、ちょっと、ちょっと。細かいことは抜きにして、これはかなり良い感じになった!
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,
  };
}

DartPadで実行
STEP6.png

うわ、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);
  });

  // 他の計算ロジックやボタンの動作に関するテストも追加できます。
}

注意:テストコードの動作確認はしてないです。

さいごに

マジ凄いっす。これは本当に凄い。
あまり動作確認はしてないので、バグはあると思いますが、許して下さい。

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?