0% found this document useful (0 votes)
78 views

Lab 6 - Basic Calculator

This document provides instructions for developing a basic calculator app in Flutter across 7 steps. It describes creating the core CalculatorApp and Calculation widgets, a ResultDisplay widget to show results, and adding number buttons in rows. Key aspects covered include making widgets stateful/stateless, setting properties like debug banners, and laying out widgets in columns and rows. The goal is to build a functional but simple calculator to learn Flutter basics.

Uploaded by

x
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
78 views

Lab 6 - Basic Calculator

This document provides instructions for developing a basic calculator app in Flutter across 7 steps. It describes creating the core CalculatorApp and Calculation widgets, a ResultDisplay widget to show results, and adding number buttons in rows. Key aspects covered include making widgets stateful/stateless, setting properties like debug banners, and laying out widgets in columns and rows. The goal is to build a functional but simple calculator to learn Flutter basics.

Uploaded by

x
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 12

Faculty of Computer Science and Information Technology

BIM30603/BIT34102 Mobile Application Development


Semester 1 2022/2023

Topic Lab 6 – Calculator App


Duration 2 hours
Tool/Software VS Code/Android Studio/Open Source IDE

Developing a basic calculator app using Flutter.


The idea is to implement a calculator whilst learning the basics of
Flutter. It should be functional but not overly complicated to
develop. For that reason, we delimit the feature set by the
following:

• It should have the possibility to let the user perform all basic
calculations: add, subtract, multiply, divide
• We do not work with float values, thus division only includes
integers
• A clean button resets the state and enables the user to define a
new calculation
• A bar at the top shows the current calculation with the first
operand, the operator and the second operand
• After a tap on the equals sign the user is presented the result of
the calculation

Implementation
1. Start a new Flutter project and replace the whole main.dart with this one:
import 'package:flutter/material.dart';
import 'calculation.dart';

void main() {
runApp(CalculatorApp());
}

class CalculatorApp extends StatelessWidget {


@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter basic calculator',
home: Scaffold(body: Calculation()),
);
}
}

The root widget of our application is a MaterialApp. This gives us a lot of predefined functionality
that are in line with Google’s Material Design. A Scaffold also provides a lot of APIs but for now
it’s only relevant for us at it makes the default font look good.

The Calculation widget is not yet created, you should get an error saying, “The method
‘Calculation’ isn’t defined for the type ‘CalculatorApp’”. We will fix it in a minute by
implementing the widget. The purpose of it is to represent the screen that holds all the UI elements
of our calculator.

Putting separate functionality in its own widget is a good practice as it encapsulates responsibility.
This is good for testing, performance, and readability.
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

2. Create new dart file named calculation.dart inside lib in project explorer and write the
following code:
import 'package:flutter/material.dart';
import 'result_display.dart';

class Calculation extends StatefulWidget {


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

class _CalculationState extends State<Calculation> {

@override
Widget build(BuildContext context) {
return ResultDisplay(text: '0');
}
}

The widget needs to be a StatefulWidget because we want to hold information like the current
result and later also the operands and the operator.

Tip: if you are unsure whether you need a StatefulWidget or a StatelessWidget, just start with a
stateless one. Android Studio and VS Code offer a shortcut to convert a StatelessWidget to a
StatefulWidget which you can use if you change your mind.

3. Create new dart file named result_display.dart inside lib in project explorer and write the
following code:

import 'package:flutter/material.dart';

class ResultDisplay extends StatelessWidget {


String text;

ResultDisplay({required this.text});

@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 80,
color: Colors.black,
child: Text(
text,
style: TextStyle(color: Colors.white, fontSize: 34),
));
}
}

We make our ResultDisplay widget a stateless widget. That’s because it does not change over
time. The only dynamic part is the text itself, which the widget gets injected in the constructor. When
the text changes, the parent widget will trigger a rebuild of this widget display the new text.

We use Dart’s syntactic sugar for constructors that do nothing else but mapping the arguments to
the class properties. This prevents a little bit of boilerplate.

Now we create a Container widget with infinite width because we want to span across the whole
width of the screen and a fix of 80 pixels. We set the color attribute to determine the background
color.
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

On top of that we want to display the result with a white color and a font size of 34. The text itself
is the result of the text property of this class which gets it in the constructor.

4. Run the app now, it should look something like this:

It’s a start, but apart from the debug banner and the status bar
overlay which we are going to take care of afterwards, the 0 is
not aligned the way we want. We want it to be centered vertically
and have a padding to the right.

There are widgets called Align and Padding which we could use.
However, we would have an unnecessary deep nesting with
Align being parent of Padding being parent of Container. In
fact, Align and Padding are nothing more than Container
widgets with certain parameters set.

So instead of nesting widgets, we just set the respective


properties of our already existing Container widget:

Widget build(BuildContext context) {


return Container(
width: double.infinity,
height: 80,
color: Colors.black,
child: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 24),
child: Text(
text,
style: const TextStyle(color: Colors.white, fontSize: 34),
),
),
);
}
}

5. Now let’s take care of the annoying debug banner and the status bar that covers our newly
implemented widget. Getting rid of the debug banner is as simple as setting the
debugShowCheckedModeBanner property in our MaterialApp:

class CalculatorApp extends StatelessWidget {


@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter basic calculator',
home: Scaffold(body: Calculation()),
);
}
}

6. Regarding the status bar, we have two options: either we extend our app into the notch and apply a
top padding so that the status bar overlaps nothing useful, or we decide to render our app content
into a SafeArea. This is a possibility to prevent interference of the UI with the OS-specific top and
bottom bars.
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

We turn the CalculatorApp into a StatefulWidget because we need the initState() method to
perform an action once. Doing such tasks in the build() is not a good practice as it’s not guaranteed
that the method is only executed once.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'calculation.dart';

void main() {
runApp(CalculatorApp());
}

class CalculatorApp extends StatefulWidget {


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

class _CalculatorAppState extends State<CalculatorApp> {


@override
void initState() {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
)
);

super.initState();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter basic calculator',
home: Scaffold(body: Calculation()),
);
}
}

7. Excluding the ResultDisplay, there are 4 Rows within the


Column. A Column allows for vertical widget placement.
The Row likewise in the horizontal direction. Modify
calculation.dart with the following code:

@override
Widget build(BuildContext context) {
return Column(children: [
ResultDisplay(text: '0'),
Row(
children: [
// Here we want to place the buttons of the
first Row
],
)
]);
}
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

8. For creating the keypad button, create new dart file named calculator_button.dart inside lib
in project explorer . Let’s design a widget first, that resembles a button on our keypad. It should be
square, show its label and have a ripple effect on tap.

import 'package:flutter/material.dart';

class CalculatorButton extends StatelessWidget {


CalculatorButton({
required this.label,
required this.onTap,
required this.size,
this.backgroundColor = Colors.white,
this.labelColor = Colors.black
});

final String label;


final VoidCallback onTap;
final double size;
final Color? backgroundColor;
final Color? labelColor;

@override
Widget build(BuildContext context) {

}
}

Again, we use the shorthand constructor to assign the arguments to the member variables:
• label: What’s written on the button, e.g. a number or an operator
• onTap: A callback that is executed whenever the button is tapped
• size: The button’s dimension. We only need one value as the buttons are supposed to be square
• backgroundColor: The background color, which defaults to white because most of the buttons
have a white background
• labelColor: The label color, which defaults to black because most of the buttons have a black
label

9. Now that we have written the constructor, let’s design the build() that actually determines the
visuals of our button in calculator_button.dart.

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(6),
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(1, 1),
blurRadius: 2
),
],
borderRadius: BorderRadius.all(
Radius.circular(size / 2)
),
color: backgroundColor
),
child: // Label text and other stuff here

)
);
}
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

We start with a Padding widget that creates an inset around its child by the specified EdgeInsets.
It’s important to do it this way and not use the padding property of the Container widget because
while the former creates a margin, the latter creates a padding and what we want is a margin between
the buttons of the keypad.

The container represents the button itself. width and height are both of the given size. In order to
mimic a button, we add a drop shadow using the BoxShadow widget and the boxShadow property of
the Container widget.

So far we have a rectangle. We turn it into a circle by using the borderRadius property with half
the size.

The color property of a Container widget determines its background color. We set it to
backgroundColor.

10. Now, let’s take care of the label and a ripple effect A FlatButton is a certain kind of
MaterialButton. These widgets take care of the button’s styling and interaction behaves like
Google’s Material Design prescribes it. Okay, while using a FlatButton would technically work,
it doesn’t seem the right thing to use here, as we display it in a different way. Actually, we only
want a button with a ripple effect. Let’s follow the recommendation of the documentation and use
an InkWell.

If we just replace the FlatButton by an InkWell, we would not see an effect. That’s because the
parent Container has a background color that would cover everything we want to see. Flutter’s
team is well aware of this circumstance and developed an Ink widget to address this issue.

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(6),
child: Ink(
width: size,
height: size,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey, offset: Offset(1, 1), blurRadius: 2),
],
borderRadius: BorderRadius.all(Radius.circular(size / 2)),
color: backgroundColor),
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(size / 2)),
),
onTap: onTap,
child: Center(
child: Text(
label,
style: TextStyle(fontSize: 24, color: labelColor),
)),
),
));
}

To maintain the circular shape, we apply a RoundedRectangleBorder as the customBorder property


of the InkWell widget.
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

11. We display the button by placing it under the ResultDisplay in calculation.dart:

import 'package:flutter/material.dart';
import 'result_display.dart';
import 'calculator_button.dart';

class Calculation extends StatefulWidget {


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

class _CalculationState extends State<Calculation> {


//int result = 0;

@override
Widget build(BuildContext context) {
return Column(children: [
ResultDisplay(text: '0'),
Row(
children: [
CalculatorButton(
label: '7',
onTap: () => {},
size: 90,
backgroundColor: Colors.white,
labelColor: Colors.black,
)
],

)
]);
}
}

We set the onTap to an empty function and the size to 90. We will change both values later.

12. Great. Now, let’s fill the rest of the keypad. To simplify this, we wrap the CalculatorButton with
a function. This way, we can define default values for the colors because most of the buttons have
the same color combination. Also, when we set the size depending on the screen width, we don’t
have to copy and paste this calculation for every Button. Write the following code at the end of
calculation.dart file.

Widget _getButton({
required String text,
required VoidCallback onTap,
Color backgroundColor = Colors.white,
Color textColor = Colors.black,
}) {
return CalculatorButton(
label: text,
onTap: onTap,
size: 90,
backgroundColor: backgroundColor,
labelColor: textColor,
);
}
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

13. Now, let’s insert every button we know we are going to need. Modify the code in Step 11:

class _CalculationState extends State<Calculation> {

@override
Widget build(BuildContext context) {
return Column(children: [
ResultDisplay(text: '0'),
Row(
children: [
_getButton(text: '7', onTap: () => numberPressed(7)),
_getButton(text: '8', onTap: () => numberPressed(8)),
_getButton(text: '9', onTap: () => numberPressed(9)),
_getButton(
text: 'x',
onTap: () => operatorPressed('*'),
backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
],
),
Row(
children: [
_getButton(text: '4', onTap: () => numberPressed(4)),
_getButton(text: '5', onTap: () => numberPressed(5)),
_getButton(text: '6', onTap: () => numberPressed(6)),
_getButton(
text: '/',
onTap: () => operatorPressed('/'),
backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
],
),
Row(
children: [
_getButton(text: '1', onTap: () => numberPressed(1)),
_getButton(text: '2', onTap: () => numberPressed(2)),
_getButton(text: '3', onTap: () => numberPressed(3)),
_getButton(
text: '+',
onTap: () => operatorPressed('+'),
backgroundColor: Color.fromRGBO(220, 220, 220, 1))
],
),
Row(
children: [
_getButton(
text: '=',
onTap: calculateResult,
backgroundColor: Colors.orange,
textColor: Colors.white),
_getButton(text: '0', onTap: () => numberPressed(0)),
_getButton(
text: 'C',
onTap: clear,
backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
_getButton(
text: '-',
onTap: () => operatorPressed('-'),
backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
],
),
]);
}
}

14. Empty functions were added after the Widget _getButton function for the callbacks being
executed when a button is pressed. By filling these with actual content, we achieve interactivity.

operatorPressed(String operator) {}
numberPressed(int number) {}
calculateResult() {}
clear() {}
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

15. Now, let’s add interactivity. We start by defining the required variables:

class _CalculationState extends State<Calculation> {


int? firstOperand;
String? operator;
int? secondOperand;
int? result;

16. Then we define what happens when the number is tapped. Modify numberPressed function from
Step 14:

numberPressed(int number) {
setState(() {
if (result != null) {
result = null;
firstOperand = number;
return;
}
if (firstOperand == null) {
firstOperand = number;
return;
}
if (operator == null) {
firstOperand = int.parse('$firstOperand$number');
return;
}
if (secondOperand == null) {
secondOperand = number;
return;
}

secondOperand = int.parse('$secondOperand$number');
});
}

There are different cases here:


• If the previous calculation is finished (thus result is not null), set the result to null and
let the number that was just pressed to the new first operand
• If the first operand is null (this is the case at the beginning or when the clear button was
pressed), set the first operand to the pressed number
• If the operator is null, pressing a number button will concat the number to the first
operand. Otherwise we could only perform one-digit operations
• Same logic applies to the second operand

17. Now let’s take care of the user pressing a button with an operator. Modify operatorPressed
function from Step 14:

operatorPressed(String operator) {
setState(() {
if (firstOperand == null) {
firstOperand = 0;
}
this.operator = operator;
});
}

Since the default value of the first operand is null, we have the case of somebody pressing an operator
button with no set first operand. In this case we treat it like zero. We set the member variable operand
to the given operand.
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

18. Okay, now let’s have a look at the function that is executed once the result button is tapped:

calculateResult() {
if (operator == null || secondOperand == null) {
return;
}
setState(() {
switch (operator) {
case '+':
result = firstOperand! + secondOperand!;
break;
case '-':
result = firstOperand! - secondOperand!;
break;
case '*':
result = firstOperand! * secondOperand!;
break;
case '/':
if (secondOperand == 0) {
return;
}
result = firstOperand! ~/ secondOperand!;
break;
}

firstOperand = result;
operator = null;
secondOperand = null;
result = null;
});
}

The first part makes the function return when either the operator or the second operand is null.
Because there is nothing to calculate if one of them is missing.

The second part is about actually performing the calculation. Symbol (!) after each variable
firstOperand and secondOperand is used as checking mechanism for detecting null value. I guess
every operator is fairly simple except for the multiply (*) operator. This is due to the fact that we
do not support float numbers. That’s why we use ~/ which performs integer division. We also
make sure that a division by zero is ruled out.

After the calculation we instantly prepare the next calculation by setting the first operand to the
result of our current calculation and resetting everything else to null.

We need to wrap everything with setState(). Otherwise the widget will not rebuild which will
result in the changes not affecting the UI.

19. We will have a look at the behavior of the clear button now:
clear() {
setState(() {
result = null;
operator = null;
secondOperand = null;
firstOperand = null;
});
}

It’s as simple as resetting every variable to null.


Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

20. So far, we only display a “0” in the ResultDisplay. However, we want to display the calculation
or the result depending on the current state. First, we create another method after clear() method:

String _getDisplayText() {
if (result != null) {
return '$result';
}

if (secondOperand != null) {
return '$firstOperand$operator$secondOperand';
}

if (operator != null) {
return '$firstOperand$operator';
}

if (firstOperand != null) {
return '$firstOperand';
}

return '0';
}

Then, instead of providing ‘0’ as the text property of the constructor of our ResultDisplay, we
choose the return value of the _getDisplayText() method.

ResultDisplay(
text: _getDisplayText()
),

This method returns the result if it is not null. If the second operand is set, it displays the whole
calculation. If the operator is set, it displays the first operand and the operator. If only the first
operator is set, it displays it. Finally if even the first operand is null (e. g. at startup time or when
the user has pressed the clean button), it displays ‘0’.

21. There is one thing left to do and that’s having the UI being responsive instead of the buttons having
a static size of 90.

class _CalculationState extends State<Calculation> {


double width = 0;

int? firstOperand;
String? operator;
int? secondOperand;
int? result;

@override
void didChangeDependencies() {
width = MediaQuery.of(context).size.width;
super.didChangeDependencies();
}

...

Widget _getButton({required String text,required VoidCallback onTap,


Color backgroundColor = Colors.white, Color textColor = Colors.black,}) {
return CalculatorButton(
label: text,
onTap: onTap,
size: width / 4 - 12,
backgroundColor: backgroundColor,
labelColor: textColor,
);
}
Faculty of Computer Science and Information Technology
BIM30603/BIT34102 Mobile Application Development
Semester 1 2022/2023

We add a member variable called width. This will hold the width of the screen. We use
didChangeDependencies() in order to obtain the screen width and set the variable to its width
value. didChangeDependencies() is called after initState(). We can not use initState()
because we need the BuildContext to obtain the screen width which is not available yet.

To determine the size of a button we simply divide the screen width by four because we have four
buttons in a row.

Final words
In this lab activity we implemented a calculator that enables the user to do basic calculations. We
extracted the responsibilities in separate widgets to keep the code readable.

Feel free to extend the example by things like floats support, addition operators like square root or a
history of all calculations.

You might also like