Creating Reusable Custom Widgets in Flutter

Learn how to design and create your own custom widgets in Flutter that you can use in any of your projects or share with the world. By Jonathan Sande.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Implementing the Play Button

You’ll handle the logic for the Play/Pause button next.

Audio widget with the Play button highlighted in red

You aren’t going to add any internal state for this button. The developer can keep track of the play state based on the audio plugin that’s actually playing the music. When that state changes, the developer can just rebuild this widget with a new value for isPlaying.

To keep the UI layout code clean, build the Play button in its own method. Go back to lib/audio_widget.dart. In _AudioWidgetState, put your cursor on IconButton, right-click and choose Refactor ▸ Extract ▸ Method from the context menu. This time, you’re extracting as a method rather than a widget so that you can keep everything in the state class.

Name the method _buildPlayPauseButton and give it this code:

IconButton _buildPlayPauseButton() {
  return IconButton(
    icon:
    (widget.isPlaying)
        ? Icon(Icons.pause)
        : Icon(Icons.play_arrow),
    color: Colors.white,
    onPressed: () {
      if (widget.onPlayStateChanged != null) {
        widget.onPlayStateChanged(!widget.isPlaying);
      }
    },
  );
}

Here are some notes about the code above:

  • IconButton now chooses an icon based on isPlaying‘s value. Pressing the button will notify anyone listening to onPlayStateChanged about the event.
  • The variables in StatefulWidget are available to the state class by prefixing them with widget.. For example, in _AudioWidgetState you can reference the isPlaying variable of AudioWidget by using widget.isPlaying.

Do a hot restart. A disadvantage of extracting to a method rather than a widget is that hot reload doesn’t work.

Press the Play button now, but there’s no response. That’s because you haven’t hooked up any logic to change the isPlaying value yet. You’ll do that once you’ve implemented all the other widgets.

Implementing the Seek Bar

Do the seek bar next because the current time label depends on it.

Audio widget with the seek bar outlined in red

Add two state variables at the top of _AudioWidgetState:

double _sliderValue;
bool _userIsMovingSlider;

The slider value can be a double from 0.0 to 1.0. Add a method at the bottom of the _AudioWidgetState class to calculate it:

double _getSliderValue() {
  if (widget.currentTime == null) {
    return 0;
  }
  return widget.currentTime.inMilliseconds / widget.totalTime.inMilliseconds;
}

Use milliseconds rather than seconds so the seek bar will move smoothly, rather than hopping from second to second.

When the user is moving the slider manually, you’ll need a method to calculate the current time based on the slider value. Add the following method at the bottom of the _AudioWidgetState class:

Duration _getDuration(double sliderValue) {
  final seconds = widget.totalTime.inSeconds * sliderValue;
  return Duration(seconds: seconds.toInt());
}

Now you can initialize the state variables. Add the following method above build in _AudioWidgetState:

@override
void initState() {
  super.initState();
  _sliderValue = _getSliderValue();
  _userIsMovingSlider = false;
}

This method is only called the first time the widget is built.

When the user is moving the seek bar at the same time that audio is playing, you don’t want _sliderValue to fight against widget.currentTime. The _userIsMovingSlider flag helps you check for that. Apply the flag by adding the following lines inside build before the return statement.

if (!_userIsMovingSlider) {
  _sliderValue = _getSliderValue();
}

Now, extract Slider into a method as you did for IconButton earlier. Put your cursor on Expanded — the parent of Slider — right-click and choose Refactor ▸ Extract ▸ Method from the context menu.

Name the method _buildSeekBar and give it the following code:

Expanded _buildSeekBar(BuildContext context) {
  return Expanded(
    child: Slider(
      value: _sliderValue,
      activeColor: Theme.of(context).textTheme.bodyText2.color,
      inactiveColor: Theme.of(context).disabledColor,
      // 1
      onChangeStart: (value) {
        _userIsMovingSlider = true;
      },
      // 2
      onChanged: (value) {
        setState(() {
          _sliderValue = value;
        });
      },
      // 3
      onChangeEnd: (value) {
        _userIsMovingSlider = false;
        if (widget.onSeekBarMoved != null) {
          final currentTime = _getDuration(value);
          widget.onSeekBarMoved(currentTime);
        }
      },
    ),
  );
}

Here are some things to note:

  1. The user is starting to manually move the Slider thumb.
  2. Whenever the Slider thumb moves, _sliderValue needs to update. This will affect the UI by updating the visual position of the thumb on the slider.
  3. When the user finishes moving the thumb, turn the flag off to start moving it based on the play position again. Then notify any listeners of the new seek position.

Do a hot restart.

Audio widget with a user moving the seek bar

The slider moves now, but the label is still not updating. You’ll address that next.

Implementing the Current Time Label

You can change the current time by changing the constructor value or by moving the slider.

Audio widget with the current time label highlighted in red

Since Slider should always stay in sync with the current time label, use _sliderValue to generate the label string.

Add the following method at the bottom of the _AudioWidgetState class:

String _getTimeString(double sliderValue) {
  final time = _getDuration(sliderValue);

  String twoDigits(int n) {
    if (n >= 10) return "$n";
    return "0$n";
  }

  final minutes = twoDigits(time.inMinutes.remainder(Duration.minutesPerHour));
  final seconds = twoDigits(time.inSeconds.remainder(Duration.secondsPerMinute));

  final hours = widget.totalTime.inHours > 0 ? '${time.inHours}:' : '';
  return "$hours$minutes:$seconds";
}

This method is a modification of the Dart Duration.toString() method.

Next, extract the current time Text widget to a method. In build, put your cursor on the first Text widget, right-click and choose Refactor ▸ Extract ▸ Method from the context menu.

Name the method _buildCurrentTimeLabel and give it the following code:

Text _buildCurrentTimeLabel() {
  return Text(
    _getTimeString(_sliderValue),
    style: TextStyle(
      fontFeatures: [FontFeature.tabularFigures()],
    ),
  );
}

FontFeature requires the dart:ui library, so add the following import at the top of the file:

import 'dart:ui';

Using FontFeature.tabularFigures() ensures that the digits will use a monospaced width. This keeps the Text width from jumping around. Read about Font Features in Flutter to learn more.

Do a hot restart.

Audio widget with current time updating as the seek bar moves

Now, the current time label updates when you move the seek bar thumb.

Implementing the Total Time Label

Last of all is the total time label on the far right.

Audio widget with the total time label outlined in red

Extract the total time Text widget to its own method. As before, in build, put your cursor on the last Text widget, right-click and choose Refactor ▸ Extract ▸ Method from the context menu.

Name the method _buildTotalTimeLabel and give it the following code:

Text _buildTotalTimeLabel() {
  return Text(
    _getTimeString(1.0),
  );
}

The total time is when the slider is all the way at the right, which is a slider value of 1.0. Thus, you can use _getTimeString() again to generate the label string.

Do a hot restart.

It looks the same as before because the totalTime argument is Duration(minutes: 1, seconds: 15), which you set previously in main.dart.

Audio widget including current time

Great! You now have your own custom widget composed completely of existing Flutter widgets.

In the last two steps, you’ll finalize your widget for production.