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 2 of 4 of this article. Click here to view the first page.

Decomposing the Design

Once you have the design you want, identify which smaller widgets you can use to build it. You should be able to get something close with IconButton, Slider, Container and a couple of Text widgets.

Audio player decomposed into widgets

Oh, yes, they’re laid out in a row, so you’ll need a Row widget, too.

Building the Basic Widget

Create a new file by right-clicking the lib folder and choosing New ▸ File. Name it audio_widget.dart.

Then enter the following code:

import 'package:flutter/material.dart';

class AudioWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Row(
        children: [
          IconButton(icon: Icon(Icons.play_arrow)),
          Text('00:37'),
          Slider(value: 0),
          Text('01:15'),
        ],
      ),
    );
  }
}

Note the Container, Row, Button, Text and Slider widgets.

Back in lib/main.dart, scroll to the bottom of the file and delete the line that says TODO delete this line. Then uncomment the line that says TODO uncomment this line.

Add the import at the top:

import 'audio_widget.dart';

Build and run the app.

This gives you the following. If you ignore the fact that it’s sitting on Beethoven’s chest, it already looks a lot like an audio player control.

Audio widget misplaced about halfway up the app view

Customizing the Look

To make the control look more like the MediaElement.js audio player, you need to make a few adjustments.

Open lib/audio_widget.dart again.

The first thing to do is give the widget a fixed height so it doesn’t take up the whole screen. Add the following line to the Container widget before its child parameter.

height: 60,

This is where hot reload shines. Press the yellow Lightning button in Android Studio to get an instant update.

That’s better. Now it’s at the bottom where it’s supposed to be:

Audio widget with a fixed height

The button looks a little too dark. That’s because it needs a function for its onPressed callback to enable it. Add onPressed: (){}, to IconButton so it looks like this:

IconButton(
  icon: Icon(Icons.play_arrow),
  onPressed: (){},
),

Do a hot reload.

The Play button is brighter now:

Audio widget with white button

There’s quite a bit you can customize about the Slider widget. Add the following parameters:

Slider(
  value: 0.5,
  activeColor: Theme.of(context).textTheme.bodyText2.color,
  inactiveColor: Theme.of(context).disabledColor,
  onChanged: (value){},
),

Here are some notes about this code:

  • A value of 0.5 puts the slider thumb in the middle.
  • Rather than hardcoding the active and inactive colors, getting the colors from the theme makes this widget work in both dark and light modes. That’s a win for reusability.
  • Giving onChanged a value enables the slider. You’ll add more code here later.

Do a hot reload.

Audio widget with slider and a lot of empty space on the right

There’s too much empty space on the right. Slider can be any length, so wrap it with Expanded. With your cursor on Slider, press Option-Return on a Mac or Alt-Enter on a PC. Choose Wrap with widget in the context menu and change widget to Expanded.

Expanded(
  child: Slider(...),
)

Do a hot reload.

Audio widget with expanded wrapped slider reaching the right edge of the screen

Looks like it needs a little padding on the right. Add SizedBox(width: 16), to the end of the list of Row children like so:

IconButton(...),
Text(...),
Slider(...),
Text(...),
SizedBox(width: 16),

Do a hot reload.

Audio widget with padding

Great! That looks pretty good for now.

Now that you’ve finished the UI, you need to allow the user to interact with the audio widget. You’ll add these UX features in the next three steps.

Determining the User Interaction

There are four pieces here:

The four parts of the audio widget's UX

  1. Play/Pause button: When a user clicks this, it should alternate between a Play and a Pause icon. When the audio reaches the end of the track, it should also revert to the Play icon. That means there needs to be a way to set the button icon, or maybe the play state.
  2. Current time: The app user doesn’t interact with the current time, but the developer needs to have some way to update it based on whatever audio plugin they’re using.
  3. Seek bar: The developer should be able to update the position based on the elapsed time of the audio that’s playing. The user should also be able to drag it to a new location and have that notify a listener.
  4. Total time: The developer needs to be able to set this based on the audio file length.
Note: This widget will not actually play any audio itself. Rather, it’s a skin that a developer can use with any audio player plugin. They’ll just rebuild the widget whenever the audio state changes. The Classical app uses the audioplayers plugin.

Defining the Parameters

Imagine that you’re a developer using this widget. How would you want to set the values?

This would be one reasonable way to do it:

AudioWidget(
  isPlaying: false,
  onPlayStateChanged: (bool isPlaying) {},
  currentTime: Duration(),
  onSeekBarMoved: (Duration newCurrentTime) {},
  totalTime: Duration(minutes: 1, seconds: 15),
),

Here’s what this code is doing:

  • isPlaying: This allows you to toggle the Play/Pause button icon.
  • onPlayStateChanged: The widget notifies you when the user presses the Play/Pause button.
  • currentTime: By using Duration here, rather than String or Text, you don’t need to worry about setting the current time text and the Slider thumb position separately. The widget will handle both of these.
  • onSeekBarMoved: This updates you when the user chooses a new location.
  • totalTime: Like currentTime, this can also be a Duration.

This is the tactic you’ll use in this tutorial.

Implementing the Parameters

There are a handful of sub-steps necessary to implement your plan above.

Converting to StatefulWidget

You originally made a stateless widget, but you need to convert it to StatefulWidget because you now have to keep track of the Slider state internally.

In lib/audio_widget.dart, put your cursor on the AudioWidget class name. Press Option-Return on a Mac or Alt-Enter on a PC to show the context menu. Choose Convert to StatefulWidget. You’ll see something similar to the following:

class AudioWidget extends StatefulWidget {
  @override
  _AudioWidgetState createState() => _AudioWidgetState();
}

class _AudioWidgetState extends State<AudioWidget> {
  @override
  Widget build(BuildContext context) {
    return Container(...);
  }
}

Adding a StatefulWidget Constructor

Now, in AudioWidget (not _AudioWidgetState), add a constructor with the parameters you defined above:

const AudioWidget({
  Key key,
  this.isPlaying = false,
  this.onPlayStateChanged,
  this.currentTime,
  this.onSeekBarMoved,
  @required this.totalTime,
}) : super(key: key);

final bool isPlaying;
final ValueChanged<bool> onPlayStateChanged;
final Duration currentTime;
final ValueChanged<Duration> onSeekBarMoved;
final Duration totalTime;

Here are some things to note:

  • The source code of the standard Flutter widgets is very useful to see how other widgets are built. The Slider widget source code is especially helpful here.
  • All widgets have keys. Watch When to Use Keys to learn more about them.
  • ValueChanged is just another name for Function(T value). This is how you make a parameter with a closure.
  • It wouldn’t make sense to have an audio player without a total time length. The @required annotation is useful to enforce that.

Since totalTime is required now, go to main.dart and add an arbitrary Duration to the AudioWidget constructor.

return AudioWidget(
  totalTime: Duration(minutes: 1, seconds: 15),
);

You’ll hook AudioWidget up to the view model later to get a real audio duration.