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:
-
IconButton
now chooses an icon based onisPlaying
‘s value. Pressing the button will notify anyone listening toonPlayStateChanged
about the event. - The variables in
StatefulWidget
are available to the state class by prefixing them withwidget.
. For example, in_AudioWidgetState
you can reference theisPlaying
variable ofAudioWidget
by 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
Slider
thumb. - 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. - 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.