Flutterで電卓アプリを作る【Flutter#1】

Flutter#1-eye Flutter

こんにちは、じあです。

今回からFlutterチュートリアルとして、初心者である私が実際に学習して制作したアプリをソースコードとともに解説するという企画をやっていきます。

なお、今回の企画ではdartファイルをデバックモードで動かすことを目標としています。

全てのソースコードは私のGitHubリポジトリにて公開しております。

手っ取り早く動かしてみたい方、全体のコードを確認したい方などなど、ぜひこちらからどうぞ!

GitHub - aitack/Flutter-tutorial
Contribute to aitack/Flutter-tutorial development by creating an account on GitHub.

導入

さっそく、今回作成するアプリについて仕様としてまとめてみます。

  1. 演算は四則演算に限る
  2. 0除算など演算エラーはちゃんとエラーとして出力する
  3. 入力のクリアボタンを設置
  4. 別タブにて計算結果を記録
  5. 4.の計算結果を削除するボタンの設置

とまぁだいたいこんな感じでやっていきます。

完成目標の画面はこんな感じ。

細かい部分は作りながら見ていきましょう。

※あくまでコードを抜粋しながらの解説となるので、コピペする場合はファイルごとダウンロードすることを推奨します。

yamlに追記

VSCodeなどでプロジェクトを作成すると、yamlが作成されますが、今回計算メソッドとして用いるパッケージをインポートできるように追記します。

pubspec.yamlの下記該当部分に追記します。インデントに注意です。

dependencies:
  flutter:
    sdk: flutter
  math_expressions: ^2.1.0 <--これを追記,インデントに注意

メインエントリーポイントの定義

Flutterアプリではmain()関数がエントリーポイントです。

この関数内でrunApp()メソッドを呼び出して、アプリのルートウィジェットを指定します。

よくわからない場合は「おまじない」だと思えばよいです。あまり本質ではないです。

void main() => runApp(const CalculatorApp());

MaterialAppウィジェットの使用

先ほど定義したCalculatorAppクラスを作成して、buildメソッド内でMaterialAppウィジェットを返すようにします。

class CalculatorApp extends StatelessWidget {

  const CalculatorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(

      debugShowCheckedModeBanner: false, //右上のデバッグラベルがつかなくなる
      title: 'Flutter Calculator',
    );
  }
}

ちなみに、このウィジェットはMaterial Designガイドラインに基づいたアプリケーションを構築するためのウィジェットとなっています。

Material Design
Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps te...

Calculatorウィジェットのスケルトンの作成

Calculatorクラスを作成して、StatefulWidgetを継承させます。

このクラス内で、アプリの状態(入力や計算結果など)を管理します。

class Calculator extends StatefulWidget {

  const Calculator({super.key});

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

続いて、_CalculatorStateクラスを作成して、このクラス内でアプリのUIとロジックを構築します。

class _CalculatorState extends State<Calculator> {
  // ここに状態を管理する変数とメソッドを定義

  @override
  Widget build(BuildContext context) {
    // ここにUIのコードを書く
  }
}

ここまでで、電卓アプリの基本的な構造が完成しました。

次のステップで、このスケルトンに実際の具体的な機能を追加していきます。

メイン画面の作成

数字と演算子のボタン

まずは数字と演算子のボタンから始めます。

完成目標はこんな感じ(再掲)。(一旦タブなどは無視してください)

よくある電卓っぽい画面です。

_CalculatorStateクラスのbuildメソッド内で、電卓のUIを構築します。

ColumnとRowウィジェットを使用して、数字と演算子ボタンを配置します。

@override
Widget build(BuildContext context) {

   return Column(
    mainAxisAlignment: MainAxisAlignment.end,
    children: <Widget>[
      Display(value: _history, isHistory: true),
      Display(value: _display),
      Row(
        children: <Widget>[
          CalculatorButton(label: '7', onPressed: _onPressed),
          // 他のボタンも同様に追加
        ],
      ),

    ],
  );
}

このあたりはTkinterや各種GUI作成に共通する部分でしょうか。

あまり長くなってもアレなので’7’の配置のみ記載しましたが、全体を確認したい方はページ冒頭のGithubのリンクからファイルごとダウンロードして確認してみてください。

なお、別ファイルcalculator_button.dartで作成したCalculatorButtonクラスを使用しています。

class CalculatorButton extends StatelessWidget {
  final String label;
  final void Function(String) onPressed;

  const CalculatorButton({super.key, required this.label, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        margin: const EdgeInsets.all(10.0),
        child: ElevatedButton(
          onPressed: () => onPressed(label),
          style: ElevatedButton.styleFrom(
            shape: const CircleBorder(),
            padding: const EdgeInsets.all(24),
            foregroundColor: Colors.black, // ボタンのテキスト色とアイコン色を黒に設定
          ),
          child: Text(
            label,
            style: const TextStyle(fontSize: 24, color: Colors.black), // テキストの色を黒に設定
          ),
        ),
      ),
    );
  }
}

ここでは具体的なデザイン(フォントや文字色、余白など)を指定しており、これを呼び出すだけで簡単にボタンを配置できるようになっています。

タブの作成

ScaffoldウィジェットのAppBarプロパティ内でbottomパラメータを使用し、TabBarウィジェットを追加します。

appBar: AppBar(
  title: Text('Flutter Calculator with Tabs'),
  bottom: TabBar(
    tabs: [
      Tab(icon: Icon(Icons.home), text: 'Calculator'),
      Tab(icon: Icon(Icons.history), text: 'History'),
    ],
  ),
),

(アイコンが用意されているのがFlutterの素晴らしい点だと思います。)

TabBarで定義した各タブに対応する内容をTabBarViewウィジェットを用いて表示します。

tabBarViewのchildrenプロパティには、各タブの内容となるウィジェットのリストを渡します。

body: TabBarView(
  children: [
    Calculator(), // ここに先ほど作成した電卓のUI
    HistoryView(), // 計算履歴を表示するウィジェット
  ],
),

続いて、DefaultTabControllerでタブを制御します。

TabBarとTabBarViewを使用するためには、それらをDefaultTabControllerウィジェットで囲ってやる必要があります。

DefaultTabControllerのlengthプロパティにはタブの数を指定します。(今回は2)

return DefaultTabController(
  length: 2, // タブの数
  child: Scaffold(
    appBar: AppBar(
      // AppBarの設定
    ),
    body: const TabBarView(
      // TabBarViewの設定
    ),
  ),
);

続いて、タブの内容を実装します。

TabBarViewで指定したウィジェットで、各タブの内容を実装します。

例えばCalculatorウィジェットで電卓のインタフェースを、HistoryViewウィジェットで計算履歴を表示するリストやログを表示します。

class HistoryTab extends StatelessWidget {
  const HistoryTab({super.key});

  @override
  Widget build(BuildContext context) {
    // 履歴を表示するUIとロジックを実装
  }

  static void addHistory(String history) {
    // 履歴を追加するロジックを実装
  }

  static void clearHistory() {
    // 履歴をクリアするロジックを実装
  }
}

計算ロジックの実装

さて、いよいよ終盤。実際に計算ロジックを実装します。

_CalculatorStateクラス内での計算処理は_evaluateメソッドによって実装しています。

String _evaluate(String expression) {
Parser p = Parser();
Expression exp;
try {
exp = p.parse(expression);
} catch (e) {
return 'Error';
}
ContextModel cm = ContextModel();
double eval = exp.evaluate(EvaluationType.REAL, cm);
return eval.toStringAsFixed(2); <--ここで表示桁を変更可
}

このメソッドはユーザーが入力した数式の文字列をパース(解析)し、計算結果を返します。

今回は計算結果の表示桁を小数第2位までとしています。

また、エラー処理も追加しています。

履歴関連の実装

ボタンを作成する際に関連付けたonPressedで履歴を追加したり、クリアボタンに関する挙動を設定しています。

void _onPressed(String value) {
    setState(() {
      if (_isResultDisplayed && RegExp(r'[0-9]').hasMatch(value)) {
        _display = value;
        _isResultDisplayed = false;
      } else if (_isResultDisplayed && RegExp(r'[+\-*/]').hasMatch(value)) {
        _display += value;
        _isResultDisplayed = false;
      } else if (value == 'C') {
        // クリアボタンが押された場合、重複する履歴を追加しない
        if (_isResultDisplayed && (_history.isNotEmpty || _display != '0')) {
          String potentialHistory = '$_history=$_display';
          if (HistoryTab.getLastHistory() != potentialHistory) {
            HistoryTab.addHistory(potentialHistory);
          }
        }
        _display = '0';
        _history = '';
        _isResultDisplayed = false;
      } else if (value == '=') {
        try {
          _history = _display;
          _display = _evaluate(_display);
          _isResultDisplayed = true;
          // '='を押したときに履歴を追加
          HistoryTab.addHistory('$_history=$_display');
        } catch (e) {
          _display = 'Error';
          _isResultDisplayed = false;
        }
      } else {
        if (_display == '0' || _isResultDisplayed) {
          _display = value;
          _isResultDisplayed = false;
        } else {
          _display += value;
        }
      }
    });
  }

クリアボタンの挙動は注意しないといけません。

プロジェクト当初、クリアボタンを押した直前の履歴が残らなくなってしまってかなりハマりました…。

続いて、HistoryTabで実際に履歴を表示する機能を実装します。

class HistoryTab extends StatelessWidget {
  static final List<String> _historyList = [];

  const HistoryTab({super.key});

  static void addHistory(String history) {
    _historyList.insert(0, history); // 新しい履歴を先頭に追加
  }

  static void clearHistory() {
    _historyList.clear(); // 履歴リストを空にする
  }

  static String getLastHistory() {
    return _historyList.isNotEmpty ? _historyList.first : '';
  }

  @override
  Widget build(BuildContext context) {
    // 計算履歴を表示するUIを構築するコードをここに記述
  }
}

完成!

基本的な構造について抜粋しながら解説してきました。

各種IDE(VSCodeなど)でFlutterプロジェクトを新規作成し、libディレクトリの中に

├flutter_application_1
|  └-lib
|     └―widgets
|     ├―calculator_button.dart
|          └―display.dart
|   └―main.dart

のように配置すればOKです。

ぜひ色々と遊んでみてください!

おわりに

今回はFlutter企画の第一弾として、電卓を作成しました。

簡単に作れると思って始めましたが、シンプルかつよく使うツールですので、色々と挙動に気を利かせるにはかなり苦労しました。

ひとまず完成としますが、例えば履歴をタップするとその数値が入力に配置され、その数値に対して計算を行えるようにする、など拡張性はまだまだ残しています。

とまぁこんな感じでかなりグダグダと勉強中のFlutterについて解説していこうと思うので、次回以降の記事も参考にしていただけると嬉しいです。

それでは、また次の記事でお会いしましょう!

タイトルとURLをコピーしました