/ octal clock

Creating your first flutter app - Part 2

Getting Started

We're going to pick up right where Part 1 left off. If you haven't already been through part 1, you should check it out for the basic setup instructions.

Download

Visit the GitHub repository to clone the source. You'll want to do this because there are over 1100 lines of code in the final app, and we won't be going over every line of code, just the important ones.

This post uses the step2 branch:

git clone https://github.com/killermonk/octal_clock_app.git
git checkout step2

Clock Faces

This is obviously not going to be a good clock app without supporting multiple clock faces. We implemented a "Text" clock face in Part 1, but we also want to add Digital and Analog clock faces.

Before we can do this, though, we need to update our octal_clock import in our pubspec.yaml to 0.0.5. I realized after writing Part 1 that we needed some more constants for the analog clock face, so I added them after the fact.

pubspec.yaml

octal_clock: "^0.0.5"

And with that, let's dive in to the code.

What we want

We have three things that we want the user to be able to control about each clock face: type, size, color.

We will create a side bar to work as our configuration area, but we're not going to cover that until Part 3. For now, you'll just have to trust me that the things I throw in to control these attributes actually work.

A Factory for Faces

We need the ability to quickly add Watch Faces and let the user pick which one they want to use. To do this, we are going to use a Library Package to contain all our Clock Faces and use Dart's Factory Constructor to create the widgets.

Since we're creating widgets, this belongs in our widgets folder, so go ahead and create lib/widgets/clock_faces/.

We're going to break the Library convention a little bit here, for the sake of organizational convenience, and create a file that will not be part of the library in this same folder. This will be our enum for what types of clock faces are available to the user. We'll start out with just the Text clock face we already created. We'll add more entries to this as we build those clock faces.

lib/src/widgets/clock_faces/clock_face_type.dart

/// The different types of clock faces that we support
enum ClockFaceType {
  text,
}

Next, lets start our package and create the widget factory
lib/src/widgets/clock_faces/clock_face_widget.dart

library clock_face_widget;

import 'dart:math';
import 'package:meta/meta.dart';
import 'package:flutter/material.dart';
import 'package:octal_clock/octal_clock.dart';

import './clock_face_type.dart';

// TODO package parts go here

/// A widget factory to create a given Clock Face based on its type
abstract class ClockFaceWidget implements StatelessWidget {
  factory ClockFaceWidget({
    @required ClockFaceType type,
    @required int hour,
    @required int minute,
    @required int second,
    Key key,
    int millisecond,
    double heightFactor = 0.5, // How we are going to control the height
    Color color = Colors.black, // Our color config, default to black
  }) {
    // factory constructors aren't allowed to have initializers, so put our asserts here
    assert(hour != null);
    assert(minute != null);
    assert(second != null);
    assert(color != null);
    assert(type != null);
    assert(heightFactor >= 0.0);
    assert(heightFactor <= 1.0);
    
    // TODO handle our constructor data

    switch (type) {
      case ClockFaceType.text:
      default:
        // TODO create the clock face
    }
  }
}

We want to be able to pass all of these values to the Clock Faces. Since we're all lazy and we don't want to copy/paste this constructor signature to all the child widgets, we are going to create a simple class to hold it all.

lib/src/widgets/clock_faces/clock_data.dart

part of clock_face_widget;

/// The data that will be passed to our ClockFaces
class _ClockData {
  final int hour;
  final int minute;
  final int second;
  final int millisecond;
  final Color color;
  final double heightFactor;

  const _ClockData({
    this.hour,
    this.minute,
    this.second,
    this.millisecond,
    this.color,
    this.heightFactor,
  });
}

Now, let's pull all of our code out of clock_page.dart and create our new TextClockFace.

lib/src/widgets/clock_faces/text_clock_face.dart

part of clock_face_widget;

String twoDigits(int d) {
  if (d < 10) return '0$d';
  return d.toString();
}

String threeDigits(int d) {
  if (d < 10) return '00$d';
  if (d < 100) return '0$d';
  return d.toString();
}

/// A clock face that displays the time as a normal looking text string
class _TextClockFace extends StatelessWidget implements ClockFaceWidget {
  final _ClockData data;

  // Size from 10.0 to 50.0
  static const MIN_SIZE = 10.0;
  static const SIZE_RANGE = 40.0;

  const _TextClockFace({Key key, @required this.data})
      : assert(data != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // Only display milliseconds if they were passed as data
    final String millisecondStr = data.millisecond != null
        ? '.${threeDigits(data.millisecond)}'
        : '';

    // Format our date string
    final String timeStr = '${twoDigits(data.hour)}:${twoDigits(data.minute)}:'
        '${twoDigits(data.second)}$millisecondStr';

    // Calculate our new font size as a percentage of SIZE_RANGE above the MIN_SIZE
    final double fontSize = MIN_SIZE + (SIZE_RANGE * data.heightFactor);

    // Keep everything the same about our text style except the font and color
    final ThemeData themeData = Theme.of(context);
    final TextStyle textStyle = themeData.textTheme.display1
        .copyWith(color: data.color, fontSize: fontSize);

    // Create the widget
    return new Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        new Text(timeStr, style: textStyle),
      ],
    );
  }
}

The only real gotcha in this code is the actual class declaration.

class _TextClockFace extends StatelessWidget implements ClockFaceWidget

First, _TextClockFace: We prefixed the class with _ to make it "package private". This prevents code outside the package from accessing it. This way they have to use our factory.
Second, implements ClockFaceWidget: To work with our factory, all the classes that are created must have a standard interface, and we make that happen by implementing the abstract classes interface.

Now, we just need to plug these two classes in to ClockFaceWidget

lib/src/widgets/clock_faces/clock_face_widget.dart

...
// Add in our Package Parts
part './clock_data.dart';
part './text_clock_face.dart';
...
    // Handle our constructor data
    final _ClockData clockData = new _ClockData(
        hour: hour,
        minute: minute,
        second: second,
        millisecond: millisecond,
        color: color,
        heightFactor: heightFactor);
...
      // Create the clock face
      case ClockFaceType.text:
      default:
        return new _TextClockFace(data: clockData);
...

Now that we've got our widget factory ready to use, we just need to plug it in to our ClockPage. We need to do 3 things: import our new files, initialize variables to track our configurable stuff, and render the new widget

lib/src/pages/clock_page.dart

// Add these imports
import '../widgets/clock_faces/clock_face_type.dart';
import '../widgets/clock_faces/clock_face_widget.dart';

...
class _ClockPageState extends State<ClockPage> {
  // Add these variables
  ClockFaceType _type;
  double _heightFactor;
  Color _color;
  
  ...
  void initState() {
    ...
    // Initialize the variables to defaults
    _type = ClockFaceType.analog;
    _heightFactor = 0.5;
    _color = null;
  }
  
  ...
  Widget build(BuildContext context) {
    // Remove the twoDigits and threeDigits functions
    
    // Figure out the color we want to use
    final ThemeData themeData = Theme.of(context);
    final Color color = _color ?? themeData.textTheme.display1.color;
    
    return new Scaffold(
      ...
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // Spruce up the date a bit, so we can shove it to the top
            new Padding(
              padding: new EdgeInsets.all(20.0),
              child: new Text(date, style: themeData.textTheme.headline),
            ),
            // Expand the clock face to fill the rest of the screen
            new Expanded(
              flex: 2,
              child: new ClockFaceWidget(
                type: _type,
                heightFactor: _heightFactor,
                hour: _time.hour,
                minute: _time.minute,
                second: _time.second,
                millisecond: _time.millisecond,
                color: color,
              ),
            ),
          ],
        ),
      ),
    );
  }

We're now at the point where we can quickly add new clock faces just by changing _type.


This is the point where you are going to want to have the GitHub repository checked out, because I'm only going to cover a few parts of the next clock faces we create. They'll each get a little more time in follow up posts.


Digital Clock Face

No clock is complete without a classic looking digital clock face, so we're going to add one. It will look like this:
Digital Clock Face

There is an easy way to do this and a hard way. The easy way to do this is to use a font and just have another Text clock face that uses this font. I chose not to do it this way for two reasons: I couldn't quickly find a free font that was also monospaced, and I really just wanted to build my own from scratch.

So, let's dive in to the Hard Way™.

Our Digital "font"

I'm only going to cover a portion of the code for our digital font in this post. For more detailed information, see the follow up post.

Our "font" is really three widgets in a library that render Custom Painters. Our three widgets are:

  1. DigitalDot: the . between seconds and milliseconds
  2. DigitalColon: the : between hours/minutes/seconds
  3. DigitalNumber: the actual numbers and digits that make up the time

Let's create our package in lib/src/widgets/digital_font
lib/src/widgets/digital_font/digital_font.dart

library digital_font;

import 'package:meta/meta.dart';
import 'package:flutter/material.dart';

part './digital_dot.dart';
part './digital_colon.dart';
part './digital_number.dart';

We'll go over a few details about digital_colon.dart to cover the basics of how we are using the Custom Painters, but you'll want to just grab the other two files from the GitHub repository.

lib/src/widgets/digital_font/digital_colon.dart

class DigitalColon extends StatelessWidget {
  final double height;
  final Color color;

  DigitalColon({Key key, @required this.height, @required this.color})
      : assert(height != null),
        assert(color != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return new CustomPaint(
      size: new Size(height / 2.0, height),
      painter: new _DigitalColonPainter(height, color),
    );
  }
}
...

There's really nothing special in the Widget portion of the file. We receive the height and the color as constructor arguments, then pass those along to a Custom Paint widget.

An important item to note: we give the Custom Paint a size. This is useful because regardless of how much we paint inside of it, it will always take up size area on the canvas. This is what allows us to make this "font" monospaced. We have defined each character as half as wide as they are tall.

Now the fun part, in the same file.

lib/src/widgets/digital_font/digital_colon.dart

...
class _DigitalColonPainter extends CustomPainter {
  final double height;
  final Color color;

  _DigitalColonPainter(this.height, this.color);

  @override
  bool shouldRepaint(_DigitalColonPainter oldDelegate) {
    return height != oldDelegate.height
        || color != oldDelegate.color;
  }

  @override
  void paint(Canvas canvas, Size size) {
    final double width = height / 2; // Half as wide as we are tall
    final double thickness = width / 5; // Arbitrary size that I liked

    final Paint paint = new Paint()
      ..color = color
      ..style = PaintingStyle.fill;

    // Top dot
    canvas.drawRect(new Rect.fromLTWH(
      width / 2 - thickness / 2,
      height / 3 - thickness / 2,
      thickness,
      thickness,
    ), paint);
    // Bottom dot
    canvas.drawRect(new Rect.fromLTWH(
      width / 2 - thickness / 2,
      height * 2 / 3 - thickness / 2,
      thickness,
      thickness,
    ), paint);
  }
}

First, shouldRepaint. Since this is a static component (a colon), we will only repaint it if we explicitly change one of its attributes.

Second, final Paint paint = new Paint().... This is where we set up our styles for how this is going to be painted. We care about two things, the color and that we fill the shapes we paint.

Third, canvas.drawRect(new Rect.fromLTWH(...), paint). This is where the magic actually happens. We create a rectangle from the left-top corner by giving it the width and the height. We want to colon centered on the canvas, so our left is half the canvas' width minus half the colon's thickness. The top locations were mostly just moving around until they were in a place I liked. The width/height of the rectangle are just the thickness of the colon, so our regtangle is actually a nice square. We pass it our previously created paint object and the canvas does the magic for us.

The DigitalNumber class, that actually does the majority of the work for this clock face, works on these same principles. It's a little more complicated because of the shape of each of the "pixels", and determining which pixels to show for each number, but it's not too complicated. We'll cover it in more detail in a follow-up post.

The actual Clock Face

The actual DigitalClockFace class is pretty straight forward, so I'm not going to go over its code in detail. We take our ClockData and we render the DigitalNumber widgets for hours, minutes, seconds, and optionally milliseconds, and throw in our DigitalColon and DigitalDot where appropriate.

There is a bug with this clock face. We currently use a Row to group our child widgets together, so if the text gets too large it overflows the display instead of wrapping to a new line.

It would make a lot more sense to use the Wrap widget for the children. This would also allow us to remove the padding widget. I'm leaving it as an exercise for the reader to implement this change.

Analog Clock Face

The analog clock face is the whole reason for the octal clock. Up to this point our clock has all been textual displays, and they just don't make all that much sense. Someone looking at a textual display will quickly get confused when the clock goes from 17 to 20, and the like. With an analog clock face, though, anyone who knows how to read a normal imperial 12-hour clock, can just innately understand what is going on. Plus, they just look good!
Analog Clock Face

Look how nicely everything can be divided! Since 8 is a power of 2, each logical segment can be evenly divided in half.

So, let's dive into a little bit of code. You'll want to referenced the GitHub repository again for the code for this file, since I'm not going over it all.

lib/src/widgets/clock_faces/analog_clock_face.dart

class _AnalogClockFace extends StatelessWidget implements ClockFaceWidget {
  final _ClockData data;

  // Size from 125.0 to 175.0
  static const MIN_SIZE = 125.0;
  static const SIZE_RANGE = 175.0;

  _AnalogClockFace({Key key, this.data})
      : assert(data != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    final double length = MIN_SIZE + (SIZE_RANGE * data.heightFactor);

    return new CustomPaint(
      size: new Size(length, length),
      painter: new _ClockDecorationPainter(data.color),
      foregroundPainter: new _ClockHandsPainter(
          data.hour, data.minute, data.second, data.color),
    );
  }
}

... custom painters are down here ...

We'll cover the custom painters for this, as well as the trigonometry for determining hand angles and lengths in a follow up post.

First, the values for MIN_SIZE and SIZE_RANGE were determined by trial and error on an iPhone 8 simulator. If we get smaller than 125 the numbers get all mashed together, and 175 seemed like a nice size that left a margin on the display.

Let's look at the CustomPaint widget we are returning, since it's different than our digital font widgets.

return new CustomPaint(
  size: new Size(length, length),
  painter: new _ClockDecorationPainter(data.color),
  foregroundPainter: new _ClockHandsPainter(
      data.hour, data.minute, data.second, data.color),
);

We broke this clock face into two painters: _ClockDecorationPainter that handles drawing everything but the hands and _ClockHandsPainter that, predictably, draws the hands of the clock. This was done for the specific purpose of optimization. There are 80 different decorations that are drawn and it's silly to redraw those all the time when they are static.

Our shouldRepaint method for decorations only repaints if the color changes. For the hands, it repaints if the color or any of the time fields change.

From the CustomPaint docs, the painter is painted "before the children" and the foregroundPainter is painted "after the children". Since this widget doesn't actually have children, this was a convenient way of grouping these two painters together and making them paint in the same space on the canvas.

Tying the Clock Faces together

Now that we've created these other two clock faces, we just have to include them as part of our ClockFaceWidget by adding them to our enum and factory constructor's switch statement.
lib/src/widgets/clock_faces/clock_face_type.dart

enum ClockFaceType {
  text,
  analog,
  digital,
}

lib/src/widgets/clock_faces/clock_face_widget.dart

...
part './digital_clock_face.dart';
part './analog_clock_face.dart';
...
abstract class ClockFaceWidget implements StatelessWidget {
...
    switch (type) {
      case ClockFaceType.analog:
        return new _AnalogClockFace(data: clockData);

      case ClockFaceType.digital:
        return new _DigitalClockFace(data: clockData);

      case ClockFaceType.text:
      default:
        return new _TextClockFace(data: clockData);
    }
...
}

And there it is! Our app now has three clock faces with configurable size and color. Our _ClockPageState is currently defaulting the type to analog, so that's the only face that will display if you run the app right now. To see the different options, update the default values in _ClockPageState.initState and relaunch the app.

The only thing left is to actually be able to let the user configure these settings. I want to be able to go into this in a little more detail, so I'm going to cover the Settings Drawer in Part 3.

Brian Armstrong

A web application developer with 15+ years programming experience. Flutter evangelist, React.js enthusiast.

Read More