Last Updated: 2020-02-21
Author: Muhammad Fathy Rashad
Credit and References: Write your first flutter app, Flutter Movie App
Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.
In this codelab, you'll create a simple mobile Flutter app. If you're familiar with object-oriented code and basic programming concepts—such as variables, loops, and conditionals—then you can complete the codelab. You don't need previous experience with Dart or mobile programming.
In this codelab, you're going to build a simple stopwatch app with neumorphism design. The app will also have the additional functionality of timer.
The following image shows the resulting app of this codelab:
You need two pieces of software—the Flutter SDK and an editor. (The codelab assumes that you're using Android Studio, but you can use your preferred editor.)
To install this you can follow the official installation guide or you can use our own guide (Windows only) for simpler setup with images or visual aid.
You can run the codelab by using any of the following devices:
If you want to compile your app to run on the web, you must enable this feature (which is currently in beta). To enable web support, use the following instructions:
$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web
You need only run the config command once. After enabling web support, every Flutter app you create also compiles for the web. In your IDE under the devices pulldown, or at the command line using flutter devices, you should now see Chrome and Web server listed. The Chrome device automatically starts Chrome. The Web server starts a server that hosts the app so that you can load it from any browser. Use the Chrome device during development so that you can use DevTools, and the web server when you want to test on other browsers. For more information, see Building a web application with Flutter and Write your first Flutter app on the web.
Create a simple, templated Flutter app by using the instructions in Create the app. Enter startup_namer (instead of myapp) as the project name. You'll modify the starter app to create the finished app.
Tip: If you don't see the ability to start a new Flutter project as an option in your IDE, then make sure that you have the plugins installed for Flutter and Dart.
You'll mostly edit lib/main.dart, where the Dart code lives.
Replace the contents of lib/main.dart.
Delete all of the code from lib/main.dart and replace it with the following code, which displays "Hello World" in the center of the screen.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: const Text('Welcome to Flutter'),
),
body: const Center(
child: const Text('Hello World'),
),
),
);
}
}
Tip: When pasting code into your app, indentation can become skewed. You can fix it with the following Flutter tools:
Run the app. You should see either Android or iOS output, depending on your device.
Android | iOS |
Tip: The first time that you run on a physical device, it can take a while to load. Afterward, you can use hot reload for quick updates. In supported IDEs, Save also performs a hot reload if the app is running. When running an app directly from the console using flutter run, enter r to perform hot reload.
Observations
In this section we will plan the app layout and add the `Stopwatch` title and style the text to look like we want.
Before we start coding anything, we should plan how we want to layout the screen to achieve our target look.
From this image, you can infer what the widget tree looks like.
Replace the Scaffold body with a Column. Column widget has a children property where you can give a list of Widgets to it. Then, put a Text widget as our header inside the Column children.
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "STOPWATCH", //Change title to Stopwatch
home: Scaffold(
appBar: AppBar(
title: const Text("STOPWATCH"), //Change title to ‘Movies'
),
body: Column( //Add from this line
children: <Widget>[
Text("STOPWATCH")
],
) //Until this line
),
);
}
}
After you change this you will see the text shows on the screen. However it looks nothing like our desired look, which is why you will stylize it.
The Text widget has a style property, which expects a TextStyle widget. In TextStyle widget you can set the font size, weight, style and many others properties.
We will increase the font size and bold it.
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: const Text('Welcome to Flutter'),
),
body: Column(
children: <Widget>[
Text("STOPWATCH", // Edit from this line
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
) // To this line
)
],
)
),
);
}
}
The result should be like this.
It looks better now, but you will notice the text is too close to the edge and we want it to have some space. The solution for this is Padding. You can wrap the Text in a Padding or Container widget. Both have a padding property where it expects EdgeInsets widget. Additionally, you want the text to be in the center, you can do this by setting the horizontal padding accordingly.
There are three types of padding that you will usually use:
We will be using symmetric padding for the above purpose. Also, we will remove the App Bar as we do not want it to show in our final app.
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
// appBar: AppBar( // remove these lines
// title: const Text('STOPWATCH'),
// ),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
)
],
)
),
);
}
}
The results should be like this:
Congratulations! Finally, the header looks like what we want. Now we can move to the next step.
In this section, we will create the UI of the stopwatch counter at the center of our appliction.
Flutter has a Container widget where we group widgets together and can customize the shape, color, shadow, etc of it. Usually we also use the Container to set the width and height of our UI as not all widget have the height and width property.
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container( // Add this line
width: 280,
height: 280,
color: Colors.grey,
) // until here
],
)
),
);
}
}
The results should be like this:
However, we can see that the result is far from our desired look. It is not shaped like a circle and does not have a nice shadow.
The Container widget can receive a `decoration` property where you can further stylize/customize the container. The `decoration` property expects a BoxDecoration widget.
Warning: You cannot use the `color` property alongside the `decoration` property, specify the color inside the BoxDecoration widget instead.
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
// color: Colors.grey.
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
)
],
)
),
);
}
}
Observation
The resulting screen should be like this:
From the previous result, you may notice that the grey color on the counter may not match the white color on the background. Now, we will change the background color to grey as well. The Scaffold widget has `backgroundColor` property where you can specify the background color.
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200, // Add this line
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
)
],
)
),
);
}
}
Result:
Although it may look the same, we have changed the background color from white to slightly grey and now you may notice the light shadow at the top left.
Next, we will add the counter icon inside the circle container. Flutter already has an Icon widget that we can use where you can also customize the size and color of the icon. Make sure you add the Icon inside a column, as we will be adding the counter text next.
> Add Column widget as the circle Container child
> Add Icon widget inside the added Column widget
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column( // Add from this line
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900)
],
), // To this line
)
],
)
),
);
}
}
Observation:
Result:
Now we have an Icon displayed on our app.
Next, we will add the text that shows the counter of the stopwatch.
> Add Text widget below the Icon before
> Stylize the Text
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00", // Add from this line
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
) // To this line
],
),
)
],
)
),
);
}
}
Result:
However, you can see that our stopwatch icon and text are aligned to the top, which is not what we want. We want it to be centered instead. Next, we will align column.
Column and Row widget has `mainAxisAlignment` property and `crossAxisAlignment` to specify the alignment. As we are using Column, the `mainAxisAlignment` will control the vertical alignment of our column.
> Set the mainAxisAlignment to center
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // Add this line
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
)
],
)
),
);
}
}
Result:
Now, we have finished the UI for our stopwatch counter. Next, we will be doing the buttons of the stopwatch.
Flutter has many build-in widget for buttons such as FlatButton, RaisedButton, etc. Most of these widgets have the `onPressed` property where you can specify the function that will be run when the button is pressed. For this tutorial, we will be using FlatButton.
> Add a new Row below the counter container before
> Add a FlatButton inside the added Row
> Add Container as the child of the FlatButton to specify the height and width
> Use Icon as the child of the added Container
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
Row( // Add from this line
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
color: Colors.white,
child: Icon(Icons.refresh, size: 60),
)
)
],
) // To this line
],
)
),
);
}
}
Now we have a clickable button, however, it looks ugly and not the UI that we want. To give the same circle shape, and shadow like the Counter circle container. Copy paste the `decoration` property.
> Copy paste the `decoration` property from the previous Container of the Counter to our Button Container
> Remove the `color` property from the Container, to avoid conflict
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
Row(
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
// color: Colors.white, // Remove this line
decoration: BoxDecoration( // Add from this line
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]), // To this line
child: Icon(Icons.refresh, size: 60),
)
)
],
)
],
)
),
);
}
}
> Repeat the previous Button, but change the icon
lib/main.dart
// ... Previous code
Row(
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.refresh, size: 60),
)
),
FlatButton( // Add from this
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.play_arrow, size: 60),
)
) // To this line
],
)
// .. The rest of the code
> Align the buttons to be centered and evenly spaced
> Add Padding to the row to add some space between the Buttons and the Counter
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
Padding( // Wrap the Row with Padding
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 60),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // Add this line
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.refresh, size: 60),
)
),
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.play_arrow, size: 60),
)
)
],
),
)
],
)
),
);
}
}
Beside Container, Flutter also has widget called Expanded. The unique thing about expanded is that instead of specifying the height and width of it, it will take as much space available. We will wrap the Counter container inside Expanded widget to force the Buttons row to the button. Previously, we did this by setting a high vertical padding for the Buttons row, but the result of this may vary depending on the device height. Hence, why we should use the Expanded widget.
> Wrap the Counter Container with Expanded widget
lib/main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Expanded( // Wrap the Container with Expanded widget
child: Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 60),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.refresh, size: 60),
)
),
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.play_arrow, size: 60),
)
)
],
),
)
],
)
),
);
}
}
Flutter widgets can be categorized into 2 types: stateless widget and stateful widget. Stateless widget is used when all the data is constant. Stateful widget is a widget where the data is dynamic or may change. For example, for our stopwatch app, the time counter inside will change over time as we press the play button, hence it need to be stateful widget.
In Android Studio, you can type `stful` and press enter. It will generate a template code for a stateful widget. There will be 2 classes generated, we will be mainly editing the one that started with underscore `_`.
> Create a new stateful widget called `StopwatchApp`
It will generate the following code:
lib/main.dart
class StopwatchApp extends StatefulWidget {
@override
_StopwatchAppState createState() => _StopwatchAppState();
}
class _StopwatchAppState extends State<StopwatchApp> {
@override
Widget build(BuildContext context) {
return Container();
}
}
> Move Scaffold widget and everything inside it into the newly created StopwatchApp widget
> Replace the `home` property value with the StopwatchApp widget instead of Scaffold
The final code you have should like the following:
lib/main.dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: StopwatchApp() // Edited
);
}
}
class StopwatchApp extends StatefulWidget {
@override
_StopwatchAppState createState() => _StopwatchAppState();
}
class _StopwatchAppState extends State<StopwatchApp> {
@override
Widget build(BuildContext context) {
return Scaffold( // Edited
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Expanded(
child: Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 60),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.refresh, size: 60),
)
),
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.play_arrow, size: 60),
)
)
],
),
)
],
)
);
}
}
Now your app should still look the same as the previous image.
Flutter has a Timer class that has a `periodic` function. The function allows us to call a given function every certain period. Therefore, we will use the Timer.periodic function to call an `update` function for every 1 millisecond. The `update` function will increase the counter by 1 and update the text displayed on the app.
Timer Class Functions:
Flutter also has a built-in Stopwatch class that emulates how stopwatch works. There are 3 functions that we can use:
(s is an instance of Stopwatch class)
And using Stopwatch class, we can also check the value of the counter/timer after it starts running and also other properties:
> Import `dart:async` package
> Create timeString variable, the text that will be displayed as the counter
> Create start() function, that will start the stopwatch
> Create update() function, that will increment the counter and update the timeString text every 1 ms
> Create reset() function, that will reset the counter
lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async'; // Add this line
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: StopwatchApp()
);
}
}
class StopwatchApp extends StatefulWidget {
@override
_StopwatchAppState createState() => _StopwatchAppState();
}
class _StopwatchAppState extends State<StopwatchApp> {
String timeString = "00:00:00"; // add from this line
Stopwatch stopwatch = Stopwatch();
Timer timer;
void start(){
stopwatch.start();
timer = Timer.periodic(Duration(milliseconds: 1), update);
}
void update(Timer t){
if(stopwatch.isRunning){
setState(() {
timeString =
(stopwatch.elapsed.inMinutes % 60).toString().padLeft(2, "0") + ":" +
(stopwatch.elapsed.inSeconds % 60).toString().padLeft(2, "0") + ":" +
(stopwatch.elapsed.inMilliseconds % 1000 / 10).clamp(0, 99).toStringAsFixed(0).padLeft(2, "0");
});
}
}
void stop(){
setState(() {
timer.cancel();
stopwatch.stop();
});
}
void reset(){
timer.cancel();
stopwatch.reset();
setState((){
timeString = "00:00:00";
});
stopwatch.stop();
} // to this line
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Expanded(
child: Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 60),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.refresh, size: 60),
)
),
FlatButton(
onPressed: (){},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.play_arrow, size: 60),
)
)
],
),
)
],
)
);
}
}
lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'STOPWATCH',
home: StopwatchApp()
);
}
}
class StopwatchApp extends StatefulWidget {
@override
_StopwatchAppState createState() => _StopwatchAppState();
}
class _StopwatchAppState extends State<StopwatchApp> {
String timeString = "00:00:00";
Stopwatch stopwatch = Stopwatch();
Timer timer;
void start(){
stopwatch.start();
timer = Timer.periodic(Duration(milliseconds: 1), update);
}
void update(Timer t){
if(stopwatch.isRunning){
setState(() {
timeString =
(stopwatch.elapsed.inMinutes % 60).toString().padLeft(2, "0") + ":" +
(stopwatch.elapsed.inSeconds % 60).toString().padLeft(2, "0") + ":" +
(stopwatch.elapsed.inMilliseconds % 1000 / 10).clamp(0, 99).toStringAsFixed(0).padLeft(2, "0");
});
}
}
void stop(){
setState(() {
timer.cancel();
stopwatch.stop();
});
}
void reset(){
timer.cancel();
stopwatch.reset();
setState((){
timeString = "00:00:00";
});
stopwatch.stop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 60),
child: Text("STOPWATCH",
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.grey.shade900,
)
),
),
Expanded(
child: Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.timer, size: 90, color: Colors.grey.shade900),
Text("00:00:00",
style: TextStyle(
fontSize: 40,
color: Colors.grey.shade900
)
)
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 60),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
onPressed: reset,
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(Icons.refresh, size: 60),
)
),
FlatButton(
onPressed: (){
stopwatch.isRunning ? stop() : start();
},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(10, 10),
color: Colors.black38,
blurRadius: 15),
BoxShadow(
offset: Offset(-10, -10),
color: Colors.white.withOpacity(0.85),
blurRadius: 15)
]),
child: Icon(stopwatch.isRunning ? Icons.pause : Icons.play_arrow, size: 60),
)
)
],
),
)
],
)
);
}
}
Congratulations, you have successfully built a Stopwatch App with Flutter!
Feel free to give a star to this workshop if you liked the tutorial.