Simple Calculator Custom Widget

This custom CalculatorScreen widget is a fully-featured calculator interface for FlutterFlow apps. It allows users to input arithmetic expressions, supports a range of operators (including percentage calculations), and updates a displayed result in real-time. Upon tapping "OK," it invokes a callback (onTap) with the evaluated result.

Key Features

  1. Expression Handling

    • Supports +, -, ×, ÷, and % operators.

    • Handles negative numbers (e.g., -5) and decimal points (e.g., 3.14).

    • Replaces percentage usage (x%) with its decimal equivalent (x/100).

  2. Real-Time Updates

    • Dynamically updates the result display after each user input.

    • If a division by zero or other invalid expression occurs, it shows an error message.

  3. Math Library Integration

    • Uses the math_expressions Dart package for parsing and evaluating expressions.

  4. User Interaction

    • Buttons arranged in a grid for digits (0-9), operators (+, -, ×, ÷, %), and actions (Clear, Backspace, OK).

    • Pressing "OK" finalizes the expression and returns the evaluated result to the parent via a Future Function(double result) callback.

  5. FlutterFlow Integration

    • Leverages FlutterFlow’s built-in theme (FlutterFlowTheme) for consistent styling across primary, secondary, and error states.

    • Uses FlutterFlow’s utility methods like formatCurrency to display the final evaluated result in a currency-friendly format (if needed).

  6. Error Handling

    • Catches division-by-zero (Infinity or NaN) and marks the calculator state with an error message.

    • Provides feedback when an operator is used incorrectly (e.g., operator at the start, except minus for negative numbers).

  7. Layout & Responsiveness

    • Accepts optional width and height parameters but can adapt to the available space if they are not provided.

    • Uses LayoutBuilder for building a responsive UI and adjusts font sizes based on widget width.

Getting Started

  1. Add Dependencies

    • Make sure you have math_expressions in your pubspec.yaml.

      dependencies:
        math_expressions: ^2.6.0
      
  2. Include the Widget

    • Import the file where you placed the CalculatorScreen class.

    • Place it in your FlutterFlow widget tree (e.g., in a custom page or custom section).

      // Automatic FlutterFlow imports
      import '/backend/schema/structs/index.dart';
      import '/backend/schema/enums/enums.dart';
      import '/backend/supabase/supabase.dart';
      import '/actions/actions.dart' as action_blocks;
      import '/flutter_flow/flutter_flow_theme.dart';
      import '/flutter_flow/flutter_flow_util.dart';
      import 'index.dart'; // Imports other custom widgets
      import '/custom_code/actions/index.dart'; // Imports custom actions
      import '/flutter_flow/custom_functions.dart'; // Imports custom functions
      import 'package:flutter/material.dart';
      // Begin custom widget code
      // DO NOT REMOVE OR MODIFY THE CODE ABOVE!
      
      import 'package:math_expressions/math_expressions.dart';
      import 'dart:ui' as ui;
      
      /// A custom calculator widget that allows performing basic arithmetic operations.
      /// It supports user-defined initial expressions and shows results in a specified currency format.
      ///
      /// This widget can be embedded anywhere in a FlutterFlow project. It uses:
      /// - MathExpressions library to parse and evaluate expressions
      /// - A custom UI layout to display input buttons and results
      /// - FlutterFlow's theme to style the calculator
      ///
      /// [onTap] callback is called when the "OK" button is pressed,
      /// passing the evaluated result as a [double].
      class CalculatorScreen extends StatefulWidget {
        /// The width of the calculator widget. If null, it uses the maximum available width.
        final double? width;
      
        /// The height of the calculator widget. If null, it uses the maximum available height.
        final double? height;
      
        /// The initial expression to display when the calculator is first shown.
        final String? initialExpression;
      
        /// A callback that returns the evaluated result of the expression when "OK" is pressed.
        final Future Function(double result) onTap;
      
        const CalculatorScreen({
          super.key,
          this.width,
          this.height,
          this.initialExpression,
          required this.onTap,
        });
      
        @override
        State<CalculatorScreen> createState() => _CalculatorScreenState();
      }
      
      class _CalculatorScreenState extends State<CalculatorScreen> {
        /// The main expression string that the user edits using the calculator UI.
        late String _expression;
      
        /// The string that holds the display of the evaluated result.
        String _result = '0';
      
        /// A flag indicating if there was an error during the last evaluation (e.g., division by zero).
        bool _hasError = false;
      
        /// A string to store the error message if an error occurs (e.g. "Division by Zero").
        String _errorMessage = '';
      
        /// List of operators recognized by the calculator.
        /// The minus sign ('-') can also be used for negative numbers.
        final List<String> _operators = ['+', '-', '×', '÷', '%'];
      
        @override
        void initState() {
          super.initState();
          // Initialize the expression with either the provided initial expression or "0".
          _expression = widget.initialExpression ?? '0';
        }
      
        /// Handles button press actions based on the button value
        /// (e.g., 'C', '⌫', 'OK', '.', operators, or numeric values).
        void _onButtonPressed(String value) {
          setState(() {
            if (value == 'C') {
              // Clear the entire expression and reset the result to '0'.
              _clear();
            } else if (value == '⌫') {
              // Delete the last character from the expression (backspace).
              _backspace();
            } else if (value == 'OK') {
              // Evaluate the expression and trigger the [onTap] callback.
              _calculateResult();
              return; // Return early to avoid _updateResult() call below.
            } else if (value == '.') {
              // Insert a decimal point.
              _addDecimal();
            } else if (_operators.contains(value)) {
              // Handle arithmetic operators such as +, -, ×, ÷, %.
              _addOperator(value);
            } else {
              // Otherwise, the value is assumed to be a digit (0-9).
              _addNumber(value);
            }
      
            // Reset error flags and messages after changing the expression
            // because a fresh input might fix or overwrite the erroneous expression.
            _hasError = false;
            _errorMessage = '';
      
            // Update the displayed result after handling the new input.
            _updateResult();
          });
        }
      
        /// Resets the entire calculator state, clearing expression and result.
        void _clear() {
          _expression = '0';
          _result = '0';
          _hasError = false;
          _errorMessage = '';
        }
      
        /// Deletes the last character in the current expression.
        /// If expression length is 1, resets expression to '0'.
        void _backspace() {
          if (_expression.length > 1) {
            _expression = _expression.substring(0, _expression.length - 1);
          } else {
            _expression = '0';
          }
          _hasError = false;
          _errorMessage = '';
        }
      
        /// Adds a decimal point ('.') to the expression.
        /// If the expression is empty or ends with an operator,
        /// "0." is inserted to ensure it's a valid decimal start.
        /// Also prevents multiple '.' in the last number.
        void _addDecimal() {
          if (_expression.isEmpty ||
              _operators.contains(_expression[_expression.length - 1])) {
            _expression += '0.';
            return;
          }
      
          // Prevent multiple decimals in the current number
          // by splitting on arithmetic operators and checking the last segment.
          final parts = _expression.split(RegExp(r'[+\-×÷%]'));
          if (!parts.last.contains('.')) {
            _expression += '.';
          }
        }
      
        /// Adds an arithmetic operator to the expression, handling some edge cases:
        /// - Allows '-' if the expression is empty (for negative numbers).
        /// - Replaces the last operator if the expression already ends with an operator.
        /// - Displays an invalid operator warning if attempting to add a non '-' operator
        ///   to an empty expression.
        void _addOperator(String operator) {
          // Allow '-' at the beginning for negative numbers.
          if (operator == '-') {
            // If expression is empty or "0", replace it with '-' for negative input.
            if (_expression.isEmpty || _expression == '0') {
              _expression = '-';
              return;
            }
      
            // If the last character is an operator, allow an additional '-'
            // to indicate a negative number for the next term.
            if (_operators.contains(_expression[_expression.length - 1])) {
              _expression += '-';
              return;
            }
          }
      
          // If the expression is empty and the operator is not '-', show a warning.
          if (_expression.isEmpty && operator != '-') {
            _showInvalidOperatorFeedback();
            return;
          }
      
          // If the last character is already an operator (and not a minus for negative),
          // replace it with the new operator instead of appending.
          if (_operators.contains(_expression[_expression.length - 1])) {
            _expression = _expression.substring(0, _expression.length - 1) + operator;
          } else {
            _expression += operator;
          }
        }
      
        /// Adds a numeric character (0-9) to the expression.
        /// If there was a division-by-zero error or any other error,
        /// attempts to replace only the zero that caused the error
        /// or reset the expression if it was "0" or if the last character is zero
        /// following an operator.
        void _addNumber(String number) {
          // If there was an error, check if the last character is zero
          // after an operator. We only want to replace that zero.
          if (_hasError) {
            // If expression length is at least 2, we can check the last two characters.
            if (_expression.length >= 2) {
              final secondLastChar = _expression[_expression.length - 2];
              final lastChar = _expression[_expression.length - 1];
              // If the last char is '0' and the previous char is one of the operators.
              if (_operators.contains(secondLastChar) && lastChar == '0') {
                // Remove the last character (zero) and keep the operator.
                _expression = _expression.substring(0, _expression.length - 1);
              }
            }
            // If the entire expression was just "0"
            else if (_expression == '0') {
              // Clear it out so the new digit can replace it entirely.
              _expression = '';
            }
      
            // Reset the error state since we're making a new input that
            // should override the previous erroneous condition.
            _hasError = false;
            _errorMessage = '';
          }
      
          // Append the new digit.
          _expression += number;
      
          // Update the result display.
          _updateResult();
        }
      
        /// Shows a brief [SnackBar] message to the user indicating invalid operator usage.
        void _showInvalidOperatorFeedback() {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Invalid operator.'),
              duration: Duration(seconds: 1),
            ),
          );
        }
      
        /// A helper method that converts any occurrence of 'x%' in the expression
        /// into its decimal equivalent (x/100).
        String _applyPercentage(String expression) {
          try {
            return expression.replaceAllMapped(RegExp(r'(\d+(\.\d+)?)%'), (match) {
              double value = double.parse(match.group(1)!);
              return (value / 100).toString();
            });
          } catch (e) {
            return expression;
          }
        }
      
        /// Validates if the expression is in a state suitable for evaluation.
        /// - Returns false if the expression is empty.
        /// - Allows a single '-' as a valid expression (e.g. negative 0).
        /// - Rejects expressions ending in an operator (unless it's just '-').
        bool _isExpressionValidForCalculation(String expr) {
          if (expr.isEmpty) return false;
      
          // A single '-' is considered valid to represent negative 0.
          if (expr == '-') {
            return true;
          }
      
          // If the last character is an operator and the expression length is more than 1,
          // it's not valid for final calculation.
          if (_operators.contains(expr[expr.length - 1]) && expr.length > 1) {
            return false;
          }
      
          return true;
        }
      
        /// Updates the [_result] by evaluating the current expression if it is valid.
        /// If evaluation fails (e.g., division by zero), sets the error flags accordingly.
        void _updateResult() {
          if (!_isExpressionValidForCalculation(_expression)) {
            _result = '0';
            return;
          }
      
          try {
            // A temporary variable to handle any final modifications before parsing.
            String finalExpr = _expression;
      
            // If the expression is just '-', treat it as '-0'.
            if (finalExpr == '-') {
              finalExpr = '-0';
            }
      
            // Replace calculator-specific symbols with standard operators
            // recognized by math_expressions (× => *, ÷ => /).
            String parsedExpression =
                _applyPercentage(finalExpr).replaceAll('×', '*').replaceAll('÷', '/');
      
            // Use math_expressions to parse and evaluate the expression.
            Parser parser = Parser();
            Expression exp = parser.parse(parsedExpression);
            ContextModel cm = ContextModel();
            double eval = exp.evaluate(EvaluationType.REAL, cm);
      
            // Check for infinities or NaN results (likely from zero division).
            if (eval.isInfinite || eval.isNaN) {
              throw Exception("Division by Zero");
            }
      
            // Format the result using the currency format defined in FlutterFlow (if any).
            _result = formatCurrency(
                eval, "\$");
      
            // Clear any previous error.
            _hasError = false;
            _errorMessage = '';
          } catch (e) {
            // If an exception is thrown, consider it an error in expression.
            _result = '';
            _hasError = true;
      
            // Check if it's specifically a "Division by Zero" error.
            _errorMessage = e.toString().contains('Division by Zero')
                ? 'Division by Zero'
                : 'Invalid Expression';
          }
        }
      
        /// Attempts to evaluate the expression, update the display,
        /// and call the [onTap] callback with the result.
        /// If the expression is invalid or leads to an error, sets the error state.
        Future<void> _calculateResult() async {
          if (!_isExpressionValidForCalculation(_expression)) {
            setState(() {
              _result = '';
              _hasError = true;
              _errorMessage = 'Invalid Expression';
            });
            return;
          }
      
          try {
            // Prepare the expression for parsing.
            String finalExpr = _expression;
            if (finalExpr == '-') {
              finalExpr = '-0';
            }
      
            String parsedExpression =
                _applyPercentage(finalExpr).replaceAll('×', '*').replaceAll('÷', '/');
      
            // Evaluate the expression using math_expressions.
            Parser parser = Parser();
            Expression exp = parser.parse(parsedExpression);
            ContextModel cm = ContextModel();
            double eval = exp.evaluate(EvaluationType.REAL, cm);
      
            // Check for infinite or NaN (zero division).
            if (eval.isInfinite || eval.isNaN) {
              throw Exception("Division by Zero");
            }
      
            // Format and display the result in the UI.
            _result = formatCurrency(
                eval,"\$");
      
            // Update the expression to the evaluated numeric result as a string
            // so the user can continue from that value.
            _expression = eval.toString();
      
            // Clear any error flags.
            _hasError = false;
            _errorMessage = '';
      
            // Trigger the callback with the evaluated result.
            await widget.onTap(eval);
          } catch (e) {
            // If any error occurred, display it as an error message.
            setState(() {
              _result = '';
              _hasError = true;
              _errorMessage = e.toString().contains('Division by Zero')
                  ? 'Division by Zero'
                  : 'Invalid Expression';
            });
          }
        }
      
        /// Builds the main UI of the calculator using [Scaffold] and [LayoutBuilder].
        /// The layout is split into two main parts:
        /// 1. The display panel (showing the expression and result)
        /// 2. The input buttons grid (digits, operators, and action buttons)
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            backgroundColor: Colors.transparent,
            body: LayoutBuilder(
              builder: (context, constraints) {
                double width = widget.width ?? constraints.maxWidth;
                double height = widget.height ?? constraints.maxHeight;
      
                return Directionality(
                  textDirection: ui.TextDirection.ltr,
                  child: Padding(
                    padding: EdgeInsets.all(16.0),
                    child: Container(
                      width: width,
                      height: height,
                      color: Colors.transparent,
                      child: Column(
                        children: [
                          _buildDisplayPanel(width, height),
                          _buildInputButtons(width, height),
                        ],
                      ),
                    ),
                  ),
                );
              },
            ),
          );
        }
      
        /// Builds the display panel, which shows the ongoing expression
        /// and the current result (or error message if applicable).
        Widget _buildDisplayPanel(double width, double height) {
          // Scale the font sizes based on the given width.
          double fontSizeExpression = width * 0.05;
          double fontSizeResult = width * 0.08;
      
          return Expanded(
            flex: 1,
            child: Container(
              decoration: BoxDecoration(
                color: FlutterFlowTheme.of(context).secondaryBackground,
                borderRadius: BorderRadius.circular(16),
              ),
              padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
              alignment: Alignment.bottomRight,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.end,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // This Flexible + SingleChildScrollView structure
                  // allows horizontal scrolling of a long expression.
                  Flexible(
                    child: SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      child: Text(
                        _expression,
                        style: TextStyle(
                          fontSize: fontSizeExpression,
                          color: FlutterFlowTheme.of(context).secondaryText,
                        ),
                        textAlign: TextAlign.left,
                        maxLines: 1,
                      ),
                    ),
                  ),
                  SizedBox(height: 8.0),
                  // Show either an error message or the result in a larger font.
                  Align(
                    alignment: Alignment.centerRight,
                    child: Text(
                      _hasError ? _errorMessage : _result,
                      style: TextStyle(
                        fontSize: fontSizeResult,
                        fontWeight: FontWeight.bold,
                        color: _hasError
                            ? FlutterFlowTheme.of(context).reed
                            : FlutterFlowTheme.of(context).primaryText,
                      ),
                      textAlign: TextAlign.right,
                      maxLines: 1,
                    ),
                  ),
                ],
              ),
            ),
          );
        }
      
        /// Builds the grid of input buttons (digits, operators, and actions).
        /// It has four rows of operators/digits, plus one additional row for action buttons.
        Widget _buildInputButtons(double width, double height) {
          // Calculate the button size so that 4 buttons fit horizontally with some spacing.
          double buttonSize = (width - 64) / 4;
      
          return Expanded(
            flex: 2,
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 16.0),
              child: Container(
                decoration: BoxDecoration(
                  color: FlutterFlowTheme.of(context).secondaryBackground,
                  borderRadius: BorderRadius.circular(16.0),
                ),
                padding: EdgeInsets.all(8),
                child: Column(
                  children: [
                    // First row: 7, 8, 9, ÷
                    _buildButtonRow(['7', '8', '9', '÷'],
                        operatorColor: FlutterFlowTheme.of(context).tertiary,
                        buttonSize: buttonSize),
                    // Second row: 4, 5, 6, ×
                    _buildButtonRow(['4', '5', '6', '×'],
                        operatorColor: FlutterFlowTheme.of(context).tertiary,
                        buttonSize: buttonSize),
                    // Third row: 1, 2, 3, -
                    _buildButtonRow(['1', '2', '3', '-'],
                        operatorColor: FlutterFlowTheme.of(context).tertiary,
                        buttonSize: buttonSize),
                    // Fourth row: 0, ., %, +
                    _buildButtonRow(['0', '.', '%', '+'],
                        operatorColor: FlutterFlowTheme.of(context).tertiary,
                        buttonSize: buttonSize),
                    // Final row: action buttons (C, ⌫, OK)
                    _buildActionButtonRow(buttonSize: buttonSize),
                  ],
                ),
              ),
            ),
          );
        }
      
        /// A helper method to generate a row of buttons (digits or operators).
        /// [operatorColor] is used for operators, while digits have a different background.
        Widget _buildButtonRow(
          List<String> buttons, {
          Color operatorColor = Colors.blue,
          required double buttonSize,
        }) {
          return Expanded(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: buttons.map((button) {
                bool isOperator = _operators.contains(button);
                return _buildButton(
                  button,
                  color: isOperator
                      ? operatorColor
                      : FlutterFlowTheme.of(context).secondaryContainer,
                  size: buttonSize,
                );
              }).toList(),
            ),
          );
        }
      
        /// Builds a row that specifically holds action buttons: [C], [⌫], and [OK].
        /// The [OK] button has a flex of 2 to make it wider.
        Widget _buildActionButtonRow({required double buttonSize}) {
          return Expanded(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _buildButton(
                  'C',
                  color: FlutterFlowTheme.of(context).reed,
                  size: buttonSize,
                ),
                _buildButton(
                  '⌫',
                  color: FlutterFlowTheme.of(context).alternate,
                  size: buttonSize,
                ),
                _buildButton(
                  'OK',
                  color: FlutterFlowTheme.of(context).green,
                  flex: 2,
                  size: buttonSize,
                ),
              ],
            ),
          );
        }
      
        /// Builds a single calculator button widget with the specified text, color, and flex.
        /// The [onPressed] callback forwards the button [text] to [_onButtonPressed].
        Widget _buildButton(
          String text, {
          Color color = Colors.blue,
          int flex = 1,
          double? size,
        }) {
          return Expanded(
            flex: flex,
            child: Padding(
              padding: const EdgeInsets.all(4.0),
              child: SizedBox(
                width: size,
                height: size,
                child: ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    padding: EdgeInsets.all(0),
                    backgroundColor: color,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16),
                    ),
                  ),
                  onPressed: () => _onButtonPressed(text),
                  child: Center(
                    child: Text(
                      text,
                      style: TextStyle(
                        fontSize: size != null ? size * 0.3 : 16,
                        fontWeight: FontWeight.bold,
                        color: FlutterFlowTheme.of(context).onSecondaryContainer,
                      ),
                    ),
                  ),
                ),
              ),
            ),
          );
        }
      }
      
      
      /// Formats a numeric [value] as a currency string using [intl.NumberFormat].
      ///
      /// Example usage:
      /// ```dart
      /// double amount = 1234.56;
      /// String symbol = '€';
      /// String result = formatCurrency(amount, symbol);
      /// // result => "€1,234.56" (depending on locale settings)
      /// ```
      String formatCurrency(double value, String currencySymbol) {
        // Create a NumberFormat for currency, specifying a locale (e.g. 'en_US').
        final formatter = NumberFormat.currency(
          locale: 'en_US',
          symbol: currencySymbol,
          // decimalDigits: 2, // Uncomment if you want to explicitly fix decimal places
        );
      
        // Return the formatted string.
        return formatter.format(value);
      }
      
      
      
      
      
      
      
      
      
      CalculatorScreen(
        width: 300.0,
        height: 400.0,
        initialExpression: '30',
        onTap: (result) async {
          // Do something with the final result
          print('Final result: $result');
        },
      )
      


  3. Styling & Theming

    • The widget uses FlutterFlowTheme extensively, so ensure you have a consistent color scheme in FlutterFlow.

    • Adjust button colors, backgrounds, and radius within the code if desired.

Why Use This Widget?

  • Convenience: Provides a plug-and-play solution to have a calculator in any part of your FlutterFlow app.

  • Customizability: You can easily customize operators, styling, and currency formats to match your needs.

  • Error Resilience: Built-in logic handles common user mistakes (like dividing by zero or incorrect operators).

With CalculatorScreen, you can seamlessly embed a powerful, user-friendly, and error-handling calculator into your FlutterFlow project. Enjoy real-time calculations and straightforward integration with the rest of your app’s logic!

3