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?
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.
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.
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:
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:
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.
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.
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.
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:
- 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.
- 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.
- 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.
- Total time: The developer needs to be able to set this based on the audio file length.
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 thanString
orText
, you don’t need to worry about setting the current time text and theSlider
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 aDuration
.
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 forFunction(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.