モード変更


    言語

Flutterを使用したデスクトップアプリの構築:チュートリアル

2023/09/04

はじめに

デスクトップ向けFlutter電卓アプリチュートリアルへようこそ!このチュートリアルでは、Googleの強力なクロスプラットフォームアプリケーション開発フレームワークであるFlutterを使用して、完全な機能を備えた電卓アプリを作成する方法を案内します。

電卓は、毎日何百万人もの人々によって使用される必須のツールです。Flutterを使って電卓アプリを作成することで、デスクトップアプリ開発の基礎を学ぶだけでなく、使いやすく効率的なインターフェースの構築についても貴重な知見を得ることができます。

このチュートリアルブログでは、ゼロから始めて、豊富な機能を備えた電卓アプリを開発するために必要な手順をすべてカバーします。Flutterの基礎知識を持っていることを前提としていますが、Flutter初心者の場合でも心配しないでください!コンセプトとコードの断片を初心者向けに説明します。

旅は、Flutterデスクトップアプリ開発のための開発環境のセットアップから始まります。インストールプロセスを案内し、必要なツールと依存関係を準備して電卓アプリのコーディングを開始できるようにします。

次に、電卓のユーザーインターフェースの設計とレイアウトについて詳しく説明します。さまざまなFlutterウィジェットとレイアウトオプションを探求し、電卓用の視覚的に魅力的で直感的なUIを作成します。ユーザーの入力を処理し、数学的な操作を実行し、リアルタイムで結果を表示する方法を学びます。

チュートリアル全体を通じて、コードの組織化、再利用性、保守性についてのベストプラクティスに焦点を当てます。コードベースを効果的に構造化し、関心事を分離し、Flutterのウィジェット構成モデルを活用してスケーラブルで拡張可能な電卓アプリを構築する方法を学びます。

このチュートリアルシリーズの最後までに、Flutterを使用してデスクトップ向けの完全な電卓アプリを構築するための確固たる理解が得られます。電卓アプリをさらに拡張やカスタマイズするためのスキルを身につけることができ、他の種類のデスクトップアプリケーションの構築にも挑戦することができます。

それでは、Flutterデスクトップアプリ開発のこのエキサイティングな旅に備えて、自分自身の電卓アプリを学び、作成し、楽しむ準備をしましょう。Flutterの世界に飛び込んでみましょう!

開発環境のセットアップ

設定の有効化:

プロジェクトのルートフォルダに移動し、macOS用のコマンドを入力します。

flutter config --enable-macos-desktop

Linuxの場合は以下のコマンドを入力します。

flutter config --enable-linux-desktop

Windowsの場合は以下のコマンドを入力します。

flutter config --enable-windows-desktop

ターミナルは再起動を求めるかもしれません。再起動後には変更はありません。 次に、ターミナルで以下のコマンドを入力します。

flutter create .

そして、次のコマンドを実行します。

flutter run -d macos

プラットフォームに応じて、macosの部分を置き換えることができます。

上記のコマンドを実行すると、macos、linux、windowsのフォルダが表示され、以下の画面が表示されます:

ユーザーインターフェースの設計

ステートクラス

CalculatorStateクラスは、Flutterアプリケーションにおける電卓の状態を表します。このクラスにはinputとresultの2つのプロパティがあります。

input:電卓でユーザーが入力した現在の入力を表します。数値、演算子、数式などのユーザーの入力を保持するString型です。

result:ユーザーの入力に基づいて計算された結果を表します。入力に対して行われた計算の結果を保持するString型です。

CalculatorStateクラスには、inputとresultのプロパティを初期化するコンストラクタがあります。コンストラクタはオプションの名前付きパラメータinputとresultを取ります。これらのパラメータは提供されない場合、デフォルトで空の文字列('')となります。

class CalculatorState {
  final String input;
  final String result;

  CalculatorState({this.input = '', this.result = ''});
}

Buttonウィジェット

Buttonクラスは、カスタマイズ可能なプロパティを持つ電卓のボタンを表すカスタムのFlutterウィジェットです。コードの異なる部分を見て、その機能を理解しましょう。

  • プロパティ:

    • buttonColor:ボタンの背景色を表します。
    • textColor:ボタンのテキストの色を表します。
    • buttonText:ボタンに表示されるテキストを表します。
    • buttontapped:ボタンがタップされたときに呼び出されるコールバック関数を表します。
  • コンストラクタ:

    • Buttonクラスには、名前付きパラメータを受け取るコンストラクタがあります:
      • buttonColor、textColor、buttonText:ボタンの外観をカスタマイズするためのオプションのパラメータです。
      • buttontapped:ボタンがタップされたときに呼び出されるVoidCallbackを必要とするパラメータです。VoidCallbackは値を返さず、ボタンがタップされたときに呼び出される関数です。

このButtonウィジェットを使用すると、buttonColor、textColor、buttonText、buttontappedコールバックなどの必要なプロパティを指定することで、FlutterのUI内にボタンを作成することができます。これにより、スタイルや動作をカスタマイズできる再利用可能なボタンを作成することができます。

import 'package:flutter/material.dart';
class Button extends StatelessWidget {

  final Color? buttonColor;
  final Color? textColor;
  final String? buttonText;
  final VoidCallback buttontapped;

  const Button({
    Key? key,
    this.buttonColor,
    this.textColor,
    required this.buttonText,
    required this.buttontapped,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: buttontapped,
      child: Padding(
        padding: const EdgeInsets.all(6),
        child: ClipRRect(
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(56),
              color: buttonColor ?? Colors.grey,
            ),
            child: Center(
              child: Text(
                buttonText ?? '',
                style: TextStyle(
                  color: textColor ?? Colors.black,
                  fontSize: 25,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Calculator画面

計算機に含まれるボタンのリスト:

final List<String> buttons = [
  'C',
  '+/-',
  '%',
  'DEL',
  '7',
  '8',
  '9',
  '/',
  '4',
  '5',
  '6',
  'x',
  '1',
  '2',
  '3',
  '-',
  '0',
  '.',
  '=',
  '+',
];

提供されたコードスニペットは、Container内にボタンが含まれるGridViewの構築を表しています。以下、詳細を説明します。

Container(
  child: GridView.builder(
    itemCount: buttons.length,
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 4,
    ),
    itemBuilder: (BuildContext context, int index) {
      if (index == 0) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 1) {
        return Button(
          buttonText: buttons[index],
          buttontapped: () {},
        );
      } else if (index == 2) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 3) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 18) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: Colors.orange,
          textColor: Colors.white,
        );
      } else {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: isOperator(buttons[index])
              ? Colors.orange
              : Colors.white30.withOpacity(0.3),
          textColor: isOperator(buttons[index])
              ? Colors.white
              : Colors.black,
        );
      }
    },
  ),
),

上記のコードは、計算機の画面を作成するために使用されるGridViewを示しています。

Container(
  child: GridView.builder(
    itemCount: buttons.length,
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 4,
    ),
    itemBuilder: (BuildContext context, int index) {
      if (index == 0) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 1) {
        return Button(
          buttonText: buttons[index],
          buttontapped: () {},
        );
      } else if (index == 2) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 3) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 18) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: Colors.orange,
          textColor: Colors.white,
        );
      } else {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: isOperator(buttons[index])
              ? Colors.orange
              : Colors.white30.withOpacity(0.3),
          textColor: isOperator(buttons[index])
              ? Colors.white
              : Colors.black,
        );
      }
    },
  ),
),

以下のコードは、buttonsリストに基づいて動的に生成されたボタンのGridViewを作成します。動作を理解するために、コードの各部分を見ていきましょう。

Container(
  child: GridView.builder(
    itemCount: buttons.length,
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 4,
    ),
    itemBuilder: (BuildContext context, int index) {
      if (index == 0) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 1) {
        return Button(
          buttonText: buttons[index],
          buttontapped: () {},
        );
      } else if (index == 2) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 3) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 18) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: Colors.orange,
          textColor: Colors.white,
        );
      } else {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: isOperator(buttons[index])
              ? Colors.orange
              : Colors.white30.withOpacity(0.3),
          textColor: isOperator(buttons[index])
              ? Colors.white
              : Colors.black,
        );
      }
    },
  ),
),

上記のコードは、Container内にボタンが含まれるGridViewを作成します。

Container(
  child: GridView.builder(
    itemCount: buttons.length,
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 4,
    ),
    itemBuilder: (BuildContext context, int index) {
      if (index == 0) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 1) {
        return Button(
          buttonText: buttons[index],
          buttontapped: () {},
        );
      } else if (index == 2) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 3) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
        );
      } else if (index == 18) {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: Colors.orange,
          textColor: Colors.white,
        );
      } else {
        return Button(
          buttontapped: () {},
          buttonText: buttons[index],
          buttonColor: isOperator(buttons[index])
              ? Colors.orange
              : Colors.white30.withOpacity(0.3),
          textColor: isOperator(buttons[index])
              ? Colors.white
              : Colors.black,
        );
      }
    },
  ),
),

これにより、buttonsリストに基づいてボタンのグリッドがContainer内に生成されます。特定のボタンには特定のカスタマイズがあり、ボタンが演算子であるかどうかに基づいて動的なスタイリングが行われます。

bool isOperator(String x) {
  if (x == '/' || x == 'x' || x == '-' || x == '+' || x == '=') {
    return true;
  }
  return false;
}

isOperatorメソッド:

このメソッドは、与えられたボタンのラベルが演算子(/、x、-、+、=)であるかどうかを判定するためのヘルパー関数です。 ボタンのラベルが演算子である場合はtrueを返し、それ以外の場合はfalseを返します。

以下は、入力スペースと結果スペースのコードです。

Expanded(
  child: SizedBox(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: <Widget>[
        Container(
          padding: const EdgeInsets.all(20),
          alignment: Alignment.centerRight,
          child: Text(
            '',
            style: const TextStyle(fontSize: 18, color: Colors.white),
          ),
        ),
        Container(
          padding: const EdgeInsets.all(15),
          alignment: Alignment.centerRight,
          child: Text(
            '',
            style: const TextStyle(
              fontSize: 30,
              color: Colors.white,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ],
    ),
  ),
),

Viewモデル

提供されたコードは、CalculatorViewModelクラスを定義しています。このクラスは、StateNotifierを拡張しています。このクラスは、計算機アプリケーションの状態とロジックを管理する責任を持ちます。コードを分解して、その機能を理解しましょう。

class CalculatorViewModel extends StateNotifier<CalculatorState> {
  CalculatorViewModel() : super(CalculatorState());
}

clearメソッドは、計算機の入力をクリアし、結果を '0'に設定します。新しいCalculatorStateのインスタンスを使用して、状態を更新します。これには、state = CalculatorState(input: '', result: '0')というコードが使われます。

void clear() {
  state = CalculatorState(input: '', result: '0');
}

equalPressedメソッドは、入力式を評価し、計算結果を更新する責任を持ちます。

  • 状態から入力を取得し、'x'を '*'に置き換えるための処理を行います。
  • 式は、math_expressionsパッケージのParserクラスを使用してパースされます。
  • パースされた式は、ExpressionクラスとContextModelを使用して評価されます。
  • 結果はdouble値として取得され、文字列に変換されてanswer変数に格納されます。
  • 最後に、修正された入力式と計算された結果で状態が更新されます。
void equalPressed() {
  String finaluserinput = state.input;
 

```dart
finaluserinput = state.input.replaceAll('x', '*');

Parser p = Parser();
Expression exp = p.parse(finaluserinput);
ContextModel cm = ContextModel();
double eval = exp.evaluate(EvaluationType.REAL, cm);
final answer = eval.toString();
state = CalculatorState(input: finaluserinput, result: answer);
}

updateInputメソッドは、与えられた操作(ボタンのラベル)を計算機の入力に追加する責任を持ちます。 状態から現在の入力を取得し、操作を連結し、更新された入力をinput変数に割り当てます。 その後、状態を更新します。結果は変更せずに、入力が変更されます。

void updateInput(String operation) {
  var input = state.input;
  input += operation;
  state = CalculatorState(input: input, result: state.result);
}

deleteメソッドは、計算機の入力から最後の文字を削除します。 状態から現在の入力を取得し、最後の文字を除いた入力のサブストリングを作成します。 修正された入力はfinalInput変数に割り当てられます。 その後、状態を更新します。結果は変更せずに、入力が変更されます。

void delete() {
  var input = state.input;
  final finalInput = input.substring(0, input.length - 1);
  state = CalculatorState(input: finalInput, result: state.result);
}

CalculatorViewModelクラスは、ボタンの押下、計算の実行、および計算機の状態の管理を担当します。StateNotifierから継承したstateプロパティを使用して、状態を更新し、状態の変更をリスナーに通知します。

UIとview modelの接続

提供されたコードでは、flutter_riverpodパッケージのStateNotifierProviderを使用して、calculatorProviderを定義します。このプロバイダは、CalculatorViewModelクラスのインスタンスを作成・管理し、その関連するCalculatorState型の状態に他の部分からアクセスできるようにします。

以下はコードの説明です:

final calculatorProvider =
    StateNotifierProvider<CalculatorViewModel, CalculatorState>(
  (ref) => CalculatorViewModel(),
);
  • calculatorProviderはfinalキーワードを使用して定数として宣言されています。
  • StateNotifierProviderのコンストラクタには2つの型パラメータが指定されています:CalculatorViewModelとCalculatorState。
  • 最初の型パラメータCalculatorViewModelは、提供される状態通知者のタイプを指定します。この場合、CalculatorViewModelクラスのインスタンスです。
  • 2番目の型パラメータCalculatorStateは、状態通知者によって管理される状態の型を指定します。ここでは、CalculatorStateクラスです。
  • コンストラクタの引数(ref) => CalculatorViewModel()は、プロバイダが最初にアクセスされたときに実行されるコールバック関数です。CalculatorViewModelクラスの新しいインスタンスを作成して返します。refパラメータはプロバイダのコンテナへのアクセスを提供し、他のプロバイダの読み取りや追加のセットアップを行うために使用できます。

calculatorProviderが定義された後、アプリケーションの他の部分はProviderContainerを使用するか、flutter_riverpodが提供するConsumerWidgetまたはConsumerウィジェットを使用して、CalculatorViewModelインスタンスとそれに関連するCalculatorStateにアクセスできます。これにより、コンポーネントは計算機の状態と対話し、ユーザーの操作や状態の変更に基づいて更新をトリガーすることができます。

UIで状態をアクセスし、それを反映させるために、build関数に以下を追加します:

final viewModel = ref.read(calculatorProvider.notifier);
final state = ref.watch(calculatorProvider);

スクリーンの完全なコードは以下の通りです:

final calculatorProvider =
    StateNotifierProvider<CalculatorViewModel, CalculatorState>(
  (ref) => CalculatorViewModel(),
);

class CalculatorScreen extends ConsumerStatefulWidget {
  const CalculatorScreen({Key? key}) : super(key: key);

  @override
  ConsumerState<CalculatorScreen> createState() =>
      _CalculatorScreenState();
}

class _CalculatorScreenState extends ConsumerState<CalculatorScreen> {
  final List<String> buttons = [
    'C',
    '+/-',
    '%',
    'DEL',
    '7',
    '8',
    '9',
    '/',
    '4',
    '5',
    '6',
    'x',
    '1',
    '2',
    '3',
    '-',
    '0',
    '.',
    '=',
    '+',
  ];

  @override
  Widget build(BuildContext context) {
    final viewModel = ref.read(calculatorProvider.notifier);
    final state = ref.watch(calculatorProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text("Calculator"),
      ),
      backgroundColor: Colors.black,
      body: Column(
        children: <Widget>[
          Expanded(
            child: SizedBox(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Container

{
  "widget": "Scaffold",
  "appBar": {
    "widget": "AppBar",
    "title": {
      "widget": "Text",
      "data": "Calculator"
    }
  },
  "backgroundColor": "Colors.black",
  "body": {
    "widget": "Column",
    "children": [
      {
        "widget": "Expanded",
        "child": {
          "widget": "SizedBox",
          "child": {
            "widget": "Column",
            "mainAxisAlignment": "MainAxisAlignment.spaceEvenly",
            "children": [
              {
                "widget": "Container",
                "padding": "const EdgeInsets.all(20)",
                "alignment": "Alignment.centerRight",
                "child": {
                  "widget": "Text",
                  "data": "${state.input}",
                  "style": "const TextStyle(fontSize: 18, color: Colors.white)"
                }
              },
              {
                "widget": "Container",
                "padding": "const EdgeInsets.all(15)",
                "alignment": "Alignment.centerRight",
                "child": {
                  "widget": "Text",
                  "data": "${state.result}",
                  "style": "const TextStyle(fontSize: 30, color: Colors.white, fontWeight: FontWeight.bold)"
                }
              }
            ]
          }
        }
      },
      {
        "widget": "Expanded",
        "flex": 3,
        "child": {
          "widget": "Container",
          "child": {
            "widget": "GridView.builder",
            "itemCount": "buttons.length",
            "gridDelegate": {
              "widget": "SliverGridDelegateWithFixedCrossAxisCount",
              "crossAxisCount": 4
            },
            "itemBuilder": "(BuildContext context, int index) {
      if (index == 0) {
        return Button(
          buttontapped: () {
            viewModel.clear();
          },
          buttonText: buttons[index],
        );
      } else if (index == 1) {
        return Button(
          buttonText: buttons[index],
          buttontapped: () {},
        );
      } else if (index == 2) {
        return Button(
          buttontapped: () {
            viewModel.updateInput(buttons[index]);
          },
          buttonText: buttons[index],
        );
      } else if (index == 3) {
        return Button(
          buttontapped: () {
            viewModel.delete();
          },
          buttonText: buttons[index],
        );
      } else if (index == 18) {
        return Button(
          buttontapped: () {
            viewModel.equalPressed();
          },
          buttonText: buttons[index],
          buttonColor: Colors.orange,
          textColor: Colors.white,
        );
      } else {
        return Button(
          buttontapped: () {
            viewModel.updateInput(buttons[index]);
          },
          buttonText: buttons[index],
          buttonColor: isOperator(buttons[index])
              ? Colors.orange
              : Colors.white30.withOpacity(0.3),
          textColor: isOperator(buttons[index])
              ? Colors.white
              : Colors.black,
        );
      }
    }"
          }
        }
      }
    ]
  }
}

最終的な画像は以下のようになります:

完全なコードはこちらで確認できます: Githubリポジトリ

記事の写真はMoataz Nabil氏によるものです。

flutterdesktopapptutorial

Author

Shaikh Huma

Shaikh Huma

Flutter Developer

Flutter Developer based in Tokyo,Japan. Flutter makes my heart flutter

その他おすすめ記事

2024/11/05

エンタープライズデータ基盤における dbt の活用戦略

近年、データ駆動型の意思決定が企業の競争力を左右する重要な要素となっており、大規模かつ複雑なデータ基盤の構築が不可欠となっています。この潮流の中で、dbt(data build tool)は、エンタープライズレベルのデータ変換とモデリングを効率化する強力なツールとして注目を集めています。 dbt は、SQL を使用してデータ変換を定義し、バージョン管理、テスト、ドキュメンテーションを統合的に行うことができるオープンソースツールです。特に以下の点で、エンタープライズデータ基盤の構築に大きな価値をもたらします...

Yoshiaki Sano

Yoshiaki Sano

Architecture

2024/11/04

データエンジニアリング初心者でも分かる!dbtの魅力と基本

データ駆動型ビジネスが当たり前となった今日、多くの企業がデータ分析の課題に直面しています。複雑な SQL クエリの管理、データの整合性確保、分析プロセスの再現性など、様々な問題が山積みです。そんな中で注目を集めているのが「dbt(data build tool)」です。 本記事では、データエンジニアリングの深い知識がなくても理解できるよう、dbt の基本と魅力について解説します。 dbt とは? dbt は、SQL を中心としたデータ変換ワークフローを管理するためのオープンソースツールです。従来の SQL...

Yoshiaki Sano

Yoshiaki Sano

Architecture

サービス開発実績会社情報
採用情報インサイトお問い合わせ
© 2022 Monstarlab
情報セキュリティ基本方針個人情報の取り扱いについて個人情報保護方針