In this tutorial, you’ll create a Tip Calculator app and learn some of the basics of developing a Flutter app. The app will let the user input - Bill Amount and Tip Percentage. Based on these values, it will calculate Tip Amount and Total Amount.
Contents
- Getting Started
- Creating a New Project
- Launching an Emulator Device
- Building UI
- Calculating Tip and Total Amount
- Testing the App Manually
- What’s Next?
Getting Started
Flutter lets you create your project on many operating systems. For example, macOS, Linux, Windows, or even Chrome OS.
Before getting started with this project, you’ll need the following things:
- Flutter - This tutorial uses Flutter 2.5
- IDE - VS Code or Android Studio - This tutorial uses VS Code.
- Dart - This tutorial uses Flutter 2.13
You can download the code used in this tutorial from this GitHub repository.
Creating a New Project
The first step is to create a new Flutter project. Open your terminal, navigate to a path of your choice and run the following command:
flutter create tip_calculator
Once your project is set up, navigate into the project directory and open it in your IDE. For VS Code, you can run the following commands:
cd tip_calculator
code .
Locate lib/main.dart file and you’ll find a lot of boilerplate code written for you. You are here to learn to write code on your own, so replace the file contents with the following code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tip Calculator',
theme: ThemeData(primarySwatch: Colors.green),
home: const FormView(),
);
}
}
As you can see in the above code, you need a FormView
screen as the home page of your app. So in the lib/main.dart, add the following code:
class FormView extends StatefulWidget {
const FormView({Key? key}) : super(key: key);
@override
_FormViewState createState() => _FormViewState();
}
class _FormViewState extends State<FormView> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Tip Calculator'),
),
);
}
@override
void dispose() {
super.dispose();
}
}
At this point, you have a MyApp
class which is a Stateless widget, and a FormView
class which is a Stateful widget.
You use a stateful widget when you want to keep track of the state of your app. The UI of a stateful widget is always the function of the state of the widget. For example, a cart page in an eCommerce app. In all other cases, a stateless widget is often handier. It always builds the same UI regardless of the state of the app. For example, the logo for your app on an appbar.
The beauty of Flutter is you can compose Stateful widgets in Stateless widgets. As you develop more and more Flutter apps, you’ll learn to use Stateless widgets more often.
Launching an Emulator Device
At this point, you have enough code to launch your app without any build errors. The next thing you need to do is to connect to a physical device or an emulator. In this tutorial, you’ll work on an emulator.
In VS Code, open the Command Palette by pressing Control-Shift-P, type Flutter: Launch Emulator, and press Enter. Select a Mobile Device emulator and wait for it to initialize.
Once your emulator is up and running, press F5 to build and run your Flutter app in debug mode. You’ll see the following screen pop up on your emulator:
This screen contains an app bar with your app title.
Building UI
Now your Flutter app is up and running, it is time to build the UI of the app. This app will have a Form with two Text fields and a Column for showing Tip Amount and Total Amount.
Adding a Form for User Input
Update the build
function of the _FormViewState
class by adding body
for your FormView
as in the following code:
body: Container(
padding: const EdgeInsets.all(16.0),
child: Form(
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Bill Amount'),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: 'Tip Percentage'),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
const SizedBox(height: 32),
// todo
],
),
),
),
Save your progress by pressing Control-S and you’ll experience the Hot Reload feature of Flutter. Whenever you update your code, Flutter only rebuilds the part of the widget tree which is dependent upon the changed code rather than rebuilding the entire app. This feature in Flutter is called Hot Reload which lets you develop applications rapidly.
Build and run:
At this point, you have a Form
with two TextFormField
widgets for Bill Amount and Tip Percentage.
One of the specifications of the app is to have default values of 0 and 15 for the Bill Amount and Total Amount fields respectively.
So, in the _FormViewState
class, add the following two class variables:
double _billAmount = 0;
double _tipPercentage = 15;
Update the TextFormField
widget for Bill Amount as in the following snippet:
initialValue: _billAmount.toString(),
Then, update the TextFormField
widget for Tip Percentage as in the following snippet:
initialValue: _tipPercentage.toString(),
This will assign a default value to both fields. Since initialValue
needs to be a string, so you need to parse the double to a string.
At this point, if you try to do a Hot Reload, it won’t update your app for the changes you have made. This is because the global and static fields relate to the state and Hot Reload excludes any changes made to the app state. In this case, you need to Hot Restart your app.
In VS Code, build and run by pressing Control-Shift-F5 for Hot restart:
At this point, you have successfully rendered your form with two text fields for taking the user input.
Adding a Container for Calculations
Your form UI is complete at this stage. Next, you need to add some Text
widgets to show the calculations for Tip Amount and Total Amount.
Add the following widget to the build
function of the _FormViewState
:
Column(
children: [
Row(
children: [
const Text('Tip Amount: '),
Text(
_totalTip.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Text('Total Amount: '),
Text(
_totalAmount.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
],
),
If you try to build and run your project, you’ll get some build errors. This is because, in the Text
widgets, you are referencing the _tipAmount
and _totalAmount
variables which you haven’t declared yet.
So, in the _FormViewState
class, add the following two class variables:
double _totalTip = 0;
double _totalAmount = 0;
These variables store the calculations related to Tip Amount and Total Amount.
Build and run your project:
At this point, you have successfully rendered a column containing two text labels to show calculations.
Calculating Tip and Total Amount
The app’s UI is complete at this stage. Next, you need to write business logic for calculating Tip Amount and Total Amount.
Updating State
In the _FormViewState
class, add the _updateAmounts
function as in the following code snippet:
void _updateAmounts() {
setState(() {
_totalTip = _billAmount * _tipPercentage / 100;
_totalAmount = _billAmount + _totalTip;
});
}
You might ask what is setState
? Well, setState
is a function provided by a Stateful widget using which you can update the state of your app. Whenever you call setState
, Flutter executes the build
function associated with the Stateful widget. This updates the UI of your app.
In the setState
function, you have used some basic maths for calculating the total tip and total amount.
In this app, you don’t have to worry about government tax. It is better to keep governments out when you are learning something.
Updating UI on User Input
One requirement of this app is to calculate Tip Amount and Total Amount as soon as the user starts typing in the form fields. For this effect, you can use the onChanged
callback provided by the TextFormField
widget. It runs whenever the user types or deletes anything in a form field. It provides the current value of a form field as a function parameter.
Add the following parameter to the TextFormField
widget for Bill Amount:
onChanged: (val) {
_billAmount = double.tryParse(val) ?? 0;
_updateAmounts();
},
Then, add the following parameter to the TextFormField
widget for Tip Percentage:
onChanged: (val) {
_tipPercentage = double.tryParse(val) ?? 0;
_updateAmounts();
},
Here what’s going on in the code above:
- You cannot assign the
val
variable to the_billAmount
variable as the former is a string and the latter is a double. Hence, you use thedouble.tryParse
function to parse a string to a double. - Since user input can be empty, so
val
can be empty, hencedouble.tryParse
can return a null value. So, you use a null check operator(??
) to assign_billAmount
to 0 when the user input is empty. - After determining the
_billAmount
, you call the_updateAmounts
function. It calculates the Tip Amount and Bill Amount and updates the UI of the app.
The same procedure applies to the working of the Tip Percentage text field.
Testing the App Manually
The coding part of your app is complete and the final step is to test whether it is doing the right calculations.
A good test covers some common use cases. You can test each of the below three test cases and make sure the app’s output matches the required output.
Bill Amount(Input) | Tip Percentage(Input) | Tip Amount(Output) | Total Amount(Output) |
---|---|---|---|
0 | 15 | 0 | 0 |
100 | 15 | 15 | 115 |
100 | 20 | 20 | 120 |
Before testing, make sure to use Hot Restart to build and run your app:
Congratulations! The app is showing accurate results. You are ready to use it when you go out for lunch and tip the waiter.
What’s Next?
In this tutorial, you learned about creating UI in Flutter, Stateless and Stateful widgets, Hot Reloading, managing state, and updating UI. You can add more features to your app, like a Number of Friends field, which you can use to divide the total amount between your friends.