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
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
).
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.
Math Library Integration
Uses the
math_expressions
Dart package for parsing and evaluating expressions.
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.
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).
Error Handling
Catches division-by-zero (
Infinity
orNaN
) 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).
Layout & Responsiveness
Accepts optional
width
andheight
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
Add Dependencies
Make sure you have
math_expressions
in yourpubspec.yaml
.dependencies: math_expressions: ^2.6.0
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'); }, )
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!