Welcome, Guest User :: Click here to login

Logo 67443

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)

README.md

Lab 1: Flutter Stopwatch

Flutter Install

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

  2. Run flutter doctor to make sure everything is installed correctly. Ideally you should get the following output:

Flutter doctor results

  1. Follow any instructions for fixing errors. Some issues students have run into are:
  • Doctor does not see your Android Studio Flutter/Dart plugins ~> install the plugins in Android Studio, then see this bugfix
  • There are no Connected Devices ~> start the iOS simulator with flutter emulators --launch ios

  1. Test your setup by building the generic Flutter project. Use VS Code to create a new Flutter project called flutter_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:

Flutter test app running


Stopwatch

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

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

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

    • First, let's change the primary swatch from blue to Colors.teal (or whatever color you like, such as red, amber, green; VS Code will give you some options).
    • Second, let's get rid of that debugging band in the upper left corner of the simulator by adding after home the line debugShowCheckedModeBanner: false,
    • Third, let's run it in the simulator and see that we have a barebones app in the color we want and no debug ribbon in the upper right corner. Not very interesting yet, so we need to move on.
  3. 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.

  4. 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;
    
    
  5. 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";
          }
        });
      }
    
    
  6. 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).

  7. 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();
               },
             )
           ]),
    
  8. 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';.

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

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

Finishing Touches

  1. 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');
    
  2. 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.

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