Lab 1: Stopwatch
Due Date: September 02
Objectives
- Learn the basics of Flutter Programing from Scratch
- Experience working with Widgets
- Practice building hybrid apps for iOS, Android, and Web (Chrome)

Due Date: September 02
Objectives
If you have not done so already install Flutter Follow the installation instructions. This may take 20-30 minutes, so please be prepared. If you haven't already, you will want to install one of the preferred IDE's like VS Code and associated Flutter & Dart plugins. (Aside: To build for Chrome you will need to switch to Flutter's beta channel.)
Run flutter doctor to make sure everything is installed correctly. Ideally you should get the following output:
~> install the plugins in Android Studio, then see this bugfix
~> start the iOS simulator with flutter emulators --launch iosflutter_test. In VS Code, this can be done easily with View > Command Palette > Flutter: New Project. After the code auto-generates, look at the lower right hand bar and see which device is selected; in these labs I will be choosing an iOS simulator because that's what I have and what I teach in other classes. Then run this generic project by pushing F5 or using VS Code option of Run > Run without debugging. Assuming everything is set up, you should see something like this:We could just try to edit the test app (that totally works) or get a little more practice by throwing away the test code and setting up a new project called 'stopwatch'. It will generate more code for us, but for today, we will do most of our work inside lib/main.dart so open the project in VS Code and go to that file.
As you can see there is a lot of boiler plate code there with lots of comments and some code to make the demo app work. Since we are going to walk through this, let's delete all the contents of this file and replace it with the following which has four main components that we will walk through and augment:
import 'package:flutter/material.dart';
// MAIN: Running our app here
void main() {
runApp(MyApp());
}
// MY_APP: A widget that sets up the barebones app
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Stopwatch'),
);
}
}
// MY_HOME_PAGE: A widget that creates a homepage for the app
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
// MY_HOME_PAGE_STATE: A widget that does the hard work of building out the details of MyHomePage
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
);
}
}
Time for a few changes...
We will start with the first component, marked off by the comment MAIN. Here we are running the function main() (which returns void) simply calls a function called runApp() which in turn runs the app we are creating (in this case, called MyApp()). This was actually generated code and we will leave it alone.
The next thing for us to look at is the component marked off with the comment MY_APP. This class basically is the shell for us to create our app. The most important line is: home: MyHomePage(title: 'Flutter Stopwatch'), (note: title can be whatever you want; 'George' works, but probably not a great choice...) This line is basically telling us that widget that is running this app is MyHomePage(). It also has a little bit of styling so let's make a few little tweaks:
Colors.teal (or whatever color you like, such as red, amber, green; VS Code will give you some options).home the line debugShowCheckedModeBanner: false,
The next component (widget) is marked off with the comment MY_HOME_PAGE. This widget starts the process of creating the home page app. It does have a final variable for the title, but the most important line is _MyHomePageState createState() => _MyHomePageState(); which uses an instance of the HomePageState class to do the actual heavy-lifting for us. We will leave this unchanged.
The final component (that will need a lot of tweaking) is MyHomePageState, indicated with the comment MY_HOME_PAGE_STATE. We are going to start by add a Stopwatch object (part of the Dart core library) and an elapsed string as variables for us to work with. We are going to also add in some integer variables for us to track the time.
Stopwatch _stopWatch = new Stopwatch();
String _elapsed = "00:00.000";
int total = 0;
int minutes = 0;
int seconds = 0;
int milli = 0;
We need a function that will update our clock, converting the milliseconds from a running stopwatch (that is coming) and converting it to minutes, seconds, and milli display values. Those values are then written as a string to our _elapsed variable.
void _updateClock() {
setState(() {
total = _stopWatch.elapsedMilliseconds;
if (total == 0) {
_elapsed = "00:00.000";
} else {
minutes = total ~/ 60000;
seconds = (total - (minutes * 60000)) ~/ 1000;
milli = total - (minutes * 60000) - (seconds * 1000);
_elapsed = "$minutes" + ":" + "$seconds" + "." + "$milli";
}
});
}
Switching gears for a moment, we have a lot of logic, but still no interface. Moving down to the @override where we give instructions for how to build the widget, you will see that we currently return a Scaffold (remember what we said in class about this?) and that right now only has an AppBar widget. After the AppBar, add the following:
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
),
),
All we are doing with this is adding a center aligned column to work with and add some interface elements to. In the blank space after alignment, we'll add some children:
children: <Widget>[
Text(
_elapsed,
style: TextStyle(fontSize: 50, fontFamily: 'Courier'),
),
],
Now when I see the reloaded interface, I see the elapsed time displayed (for now, all zeros).
After the elapsed time text, we are going to add a Row widget that will have two buttons, one for stop/reset of the timer and the other to start it. Here is some code to accomplish that:
Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
OutlinedButton.icon(
label: Text('Stop'),
icon: Icon(Icons.stop),
onPressed: () {
if (_stopWatch.isRunning)
_stopWatch.stop();
else
_stopWatch.reset();
},
),
OutlinedButton.icon(
label: Text('Play'),
icon: Icon(Icons.play_arrow),
onPressed: () {
_stopWatch.start();
},
)
]),
Of course, when we push the buttons nothing happens. Actually, something is happening -- the stopwatch object is starting and stopping -- but we don't see any evidence of this. To make this work, we are going to have to call the update_clock method we wrote earlier. To update this with some regularity, we need a Timer object that is part of the dart:async library. We'll go to the top of the code and right after the line importing the core dart library, we'll add import 'dart:async';.
Now that we have access to a Timer, let's add one right after our int properties with the following code:
Timer? timer;
static const duration = const Duration(milliseconds: 69);
Do you recall in class why we set the duration to 69 and not some even number like 50 or 100? If not, try those values later and see how it feels in operation.
Now that we have a countdown timer, we have to use it to fire off some update_clock calls so the display is periodically updated. Right before we create the Scaffold, let's add the following:
if (timer == null)
timer = Timer.periodic(duration, (Timer t) {
_updateClock();
});
Do a hot reload and see your app in action.
One thing that is a little distracting is that the clock minutes and seconds vary between one and two digits, causing a little shifting. To fix that, let's change the line in update_clock where we do a simple updating of elapsed with the following:
_elapsed = "$minutes".padLeft(2, '0') +
":" +
"$seconds".padLeft(2, '0') +
"." +
"$milli".padLeft(3, '0');
It would be nice to have an AppIcon for our app. You can download the AppIcon we have for Stopwatch and add this a new folder called assets/icon/ saving the largest image as icon.png. Then update pubspec.yaml file to include:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: "^0.8.0"
flutter_icons:
android: "launcher_icon"
ios: true
image_path: "assets/icon/icon.png"
Then run the commands:
flutter pub get
flutter pub run flutter_launcher_icons:main
For more info about updating the icon visit here.
So far we have run this app in a simulator, but if you want to deploy to an iOS device (a topic we will cover later), there are some instructions here. Instructions for Android can be found here.