in Mobile

Create a Stopwatch App with Flutter

Flutter - Beautiful native apps in record time

I’ve been learning Flutter and Dart language recently and they really impressed me. Flutter is greatly inspired by React and many concepts are already familiar: stateful/stateless, render function, component hierarchy, etc. As for Dart language which backs Flutter, it inherits numerous of the best features from other languages while keeping off from the bad things, so if you already know python, JavaScript, C++, you can pick up Dart very quickly.

Okay, let’s go back to the main topic of this article: creating a stopwatch app. There are some posts about how to build a stopwatch (like this), but I would like to share an alternative approach.

The example in this post used Android Studio. If you haven’t done so, please follow the “Get started” part in the Flutter docs to setup Flutter and Android Studio.

The Idea

Flutter provided a Stopwatch class. It can be started or stopped, and the elapsed time can be read from the elapsedMilliseconds property. So technically we can set up a page with a Stopwatch instance, then simply display the stopwatch.elapsedMilliseconds.

However, there is one problem: Stopwatch does not provide any callbacks, so we have no idea when to perform a re-render. Here the Timer class comes to the rescue. It triggers a callback with a given interval. Therefore, we can use a Timer to trigger the re-render, read stopwatch.elapsedMilliseconds and rebuild the page.

Implementation

Create a new Flutter project in Android Studio, then replace the main.dart with the following code.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

String formatTime(int milliseconds) {
  var secs = milliseconds ~/ 1000;
  var hours = (secs ~/ 3600).toString().padLeft(2, '0');
  var minutes = ((secs % 3600) ~/ 60).toString().padLeft(2, '0');
  var seconds = (secs % 60).toString().padLeft(2, '0');

  return "$hours:$minutes:$seconds";
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Stopwatch Example', home: StopwatchPage());
  }
}

class StopwatchPage extends StatefulWidget {
  @override
  _StopwatchPageState createState() => _StopwatchPageState();
}

class _StopwatchPageState extends State<StopwatchPage> {
  Stopwatch _stopwatch;

  @override
  void initState() {
    super.initState();
    _stopwatch = Stopwatch();
  }

  void handleStartStop() {
    if (_stopwatch.isRunning) {
      _stopwatch.stop();
    } else {
      _stopwatch.start();
    }
    setState(() {});    // re-render the page
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stopwatch Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(formatTime(_stopwatch.elapsedMilliseconds), style: TextStyle(fontSize: 48.0)),
            ElevatedButton(onPressed: handleStartStop, child: Text(_stopwatch.isRunning ? 'Stop' : 'Start')),
          ],
        ),
      ),
    );
  }
}

The main class StopwatchPage is a StatefulWidget, who created a Stopwatch instance _stopwatch. The _stopwatch.elapsedMilliseconds is formatted by formatTime function and displayed on the page. A button controls the start/stop of the _stopwatch. Very simple setup.

Run this app and you’ll see the above page. You’ll also notice that the stopwatch does not increase after the “Start” button is clicked. It is actually running, because if you click the “Start” / “Stop” button repeatedly, you’ll see the time increases. That’s the problem we discussed in the “The Idea” section – we don’t know when to re-render the page. The _stopwatch is running in the background, it is simply not displayed.

We will add a Timer instance to help the re-render.

  Timer _timer;

  @override
  void initState() {
    super.initState();
    _stopwatch = Stopwatch();
    // re-render every 30ms
    _timer = new Timer.periodic(new Duration(milliseconds: 30), (timer) {
      setState(() {});
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

In the above code, we created a Timer instance that will call setState every 30 milliseconds. This will help us re-render the page and reflect the actual stopwatch value.

Now restart the app (hot reloading won’t work since the changes occurred in the initState() ). Click the “Start” button and you will see the stopwatch working correctly.

Conclusion

You can access my GitHub to see the complete code of this post.

In short, a stopwatch consists of two parts: a Stopwatch instance to count the time, and a Timer instance to render the page.

Of course, this app can be further improved, e.g. improve the Timer callback so that re-render is triggered only if the formatted time string is changed. To keep this post short, I’ll leave it to the readers.

But keep in mind that, a Stopwatch instance created in the UI widget WILL NOT survive if the app goes to the background. My tests on Android showed that the stopwatch will stop counting a couple of minutes after the app went inactive.

So the stopwatch can only be used when the app is active. To create a real stopwatch that runs even if the app is inactive, some advanced approaches are required, e.g. a Foreground Service needs to be created on Android.

Write a Comment

Comment