Flutter Beginner Lesson 124 of 135 4 min read
Calculator App in Flutter (Beginner Project with Source Code)
Build a calculator app in Flutter: a button grid with Row, Column and Expanded, + − × ÷, percent and error handling. Beginner project, full runnable code.
On this page
The calculator is the classic first Flutter project. It practises layout (a grid of buttons with Row, Column and Expanded), state with setState, and logic separated from the UI.
The full app
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: CalculatorPage(),
));
class CalculatorPage extends StatefulWidget {
const CalculatorPage({super.key});
@override
State<CalculatorPage> createState() => _CalculatorPageState();
}
class _CalculatorPageState extends State<CalculatorPage> {
String display = '0';
double? first; // the number before the operator
String? operator; // + − × ÷
bool fresh = true; // the next digit starts a new number
static const rows = [
['C', '±', '%', '÷'],
['7', '8', '9', '×'],
['4', '5', '6', '−'],
['1', '2', '3', '+'],
['0', '.', '='],
];
double calculate(double a, double b, String op) => switch (op) {
'+' => a + b,
'−' => a - b,
'×' => a * b,
'÷' => b == 0 ? double.nan : a / b,
_ => b,
};
String format(double v) {
if (v.isNaN || v.isInfinite) return 'Error';
if (v == v.roundToDouble() && v.abs() < 1e12) return v.toInt().toString();
var s = v.toStringAsFixed(8).replaceFirst(RegExp(r'0+$'), '');
return s.endsWith('.') ? s.substring(0, s.length - 1) : s;
}
void press(String key) {
setState(() {
if (display == 'Error') {
display = '0';
first = null;
operator = null;
fresh = true;
if (key == 'C') return;
}
switch (key) {
case 'C':
display = '0';
first = null;
operator = null;
fresh = true;
case '±':
if (display != '0') {
display = display.startsWith('-') ? display.substring(1) : '-$display';
}
case '%':
display = format(double.parse(display) / 100);
case '+' || '−' || '×' || '÷':
if (first != null && operator != null && !fresh) {
display = format(calculate(first!, double.parse(display), operator!));
}
first = double.tryParse(display);
operator = key;
fresh = true;
case '=':
if (first != null && operator != null) {
display = format(calculate(first!, double.parse(display), operator!));
first = null;
operator = null;
fresh = true;
}
case '.':
if (fresh) {
display = '0.';
fresh = false;
} else if (!display.contains('.')) {
display += '.';
}
default: // a digit
if (fresh || display == '0') {
display = key;
fresh = false;
} else if (display.length < 12) {
display += key;
}
}
});
}
Color colorFor(String key, ColorScheme c) {
if ('÷×−+='.contains(key)) return c.primary;
if ('C±%'.contains(key)) return c.secondaryContainer;
return c.surfaceContainerHighest;
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.bottomRight,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
first == null ? '' : '${format(first!)} ${operator ?? ''}',
style: TextStyle(fontSize: 22, color: colors.outline),
),
FittedBox(
child: Text(display, style: const TextStyle(fontSize: 64, fontWeight: FontWeight.w300)),
),
],
),
),
),
for (final row in rows)
Row(
children: [
for (final key in row)
Expanded(
flex: key == '0' ? 2 : 1,
child: Padding(
padding: const EdgeInsets.all(5),
child: SizedBox(
height: 68,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundColor: colorFor(key, colors),
foregroundColor: '÷×−+='.contains(key) ? colors.onPrimary : colors.onSurface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
),
onPressed: () => press(key),
child: Text(key, style: const TextStyle(fontSize: 26)),
),
),
),
),
],
),
],
),
),
),
);
}
}
How it works
Layout. The screen is a Column: an Expanded display area at the top, then one Row per line of buttons. Each button is wrapped in Expanded, so a row’s buttons share the width equally; flex: 2 makes 0 twice as wide. Learn more in Row and Column and Expanded.
State. Four fields describe everything: what is on the display, the first number, the operator, and whether the next digit starts a new number. Every button calls press, which changes those fields inside setState.
Logic. calculate is a switch expression, and case '+' || '−' || '×' || '÷': uses an or-pattern to handle four buttons in one place. Pressing an operator twice in a row (2 + 3 +) calculates the running total, like a real calculator.
Errors. Dividing by zero gives NaN, which format shows as “Error”; the next key press clears it.
Make it yours
- Add a history list of past calculations below the display.
- Support the keyboard on desktop and web with a
Focuswidget andonKeyEvent. - Add a dark mode switch (themes and dark mode).
- Move the logic into a separate
Calculatorclass and write unit tests for it.