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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating Reusable Custom Widgets in Flutter
25 mins
- Getting Started
- Refactoring UI Layouts
- Extracting Widgets
- Types of Custom Widgets
- Designing Your Widget
- Decomposing the Design
- Building the Basic Widget
- Customizing the Look
- Determining the User Interaction
- Defining the Parameters
- Implementing the Parameters
- Converting to StatefulWidget
- Adding a StatefulWidget Constructor
- Implementing the Play Button
- Implementing the Seek Bar
- Implementing the Current Time Label
- Implementing the Total Time Label
- Testing the Widget
- Sharing Your Widget With the World
- Where to Go From Here?
Implementing the Play Button
You’ll handle the logic for the Play/Pause button next.
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:
-
IconButtonnow chooses an icon based onisPlaying‘s value. Pressing the button will notify anyone listening toonPlayStateChangedabout the event. - The variables in
StatefulWidgetare available to the state class by prefixing them withwidget.. For example, in_AudioWidgetStateyou can reference theisPlayingvariable ofAudioWidgetby usingwidget.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.
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:
- The user is starting to manually move the
Sliderthumb. - Whenever the
Sliderthumb moves,_sliderValueneeds to update. This will affect the UI by updating the visual position of the thumb on the slider. - 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.
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.
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.
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.
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.
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.





