Last Updated: 2020-02-21

Author: Muhammad Fathy Rashad

Credit and References: Write your first flutter app, Flutter Movie App

What is Flutter?

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.

What you'll build

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:

What you'll learn

What you'll need

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.

lib/main.dart

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.

Plan the app layout

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.

Add header text

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.

Stylize text

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.

Add padding

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.

Add Container Widget

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.

Stylize Container with BoxDecoration

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:

Set Background Color

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.

Add Counter Icon

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.

Add Counter Text

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.

Aligning 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.

Stylize the Button

> 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),
                  )
                )
              ],
            )
          ],
        )
      ),
    );
  }
}

Add the Play Button

> 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 and Pad the Button

> 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),
                      )
                  )
                ],
              ),
            )
          ],
        )
      ),
    );
  }
}

Expanded Widget

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),
                      )
                  )
                ],
              ),
            )
          ],
        )
      ),
    );
  }
}

Stateful Widget

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.

Creating 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 App to Stateful Widget

> 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.

Timer class

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:

Stopwatch Class

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:

Add Stopwatch Functions

> 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.