Dart Extensions Tutorial: Improve your Flutter Code
Learn how to take your Flutter skills to the next level and make your code reusable with one of Dart’s most useful features: Dart extensions. By Sébastien Bel.
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
Dart Extensions Tutorial: Improve your Flutter Code
30 mins
- Getting Started
- What Is an Extension Method?
- Purpose
- Comparison with Alternatives
- Creating and Using a Basic Extension
- Syntax
- Creating StringCaseConverter Extension
- Advanced Usages
- Nullable Types
- Generics
- Private Dart Extensions
- Static Functions, Constructors and Factories
- Dart Extensions on Enums
- Handling Conflicts
- Common Extension Usages
- Adding Features to Classes
- Dart Extensions as Shortcuts
- Popular Packages Using Extensions
- Extensions Everywhere ... Or not?
- Where to Go From Here?
Advanced Usages
Dart extensions can go way beyond simple String
transformations. You can extend nullable types and generics and can even create private extensions.
Nullable Types
The cat’s weight comments don’t start with an uppercase. You’ll correct it using a slightly modified version of StringCaseConverter
.
Look at the _catWeightCommentBuilder()
method in lib/main.dart.
If you’d like to use firstLetterUppercase()
on _catWeightComment
, you’d have to deal with the fact that the _catWeightComment
variable is nullable.
It could look like this:
_catWeightComment?.firstLetterUppercase()
Note the ?
to handle nullable values.
But there’s an even easier approach: You can make extensions on nullable types.
Replace StringCaseConverter
in lib/utils/string_case_converter.dart with this code:
extension StringCaseConverter on String? {
String firstLetterUppercase() {
if (this == null || this!.isEmpty) {
return '';
} else {
final firstLetter = this!.substring(0, 1);
final rest = this!.substring(1, this!.length);
return firstLetter.toUpperCase() + rest;
}
}
}
Because you handle the nullable values in firstLetterUppercase()
, you don’t need the ?
on your method calls anymore.
Go back to lib/main.dart and change _catWeightCommentBuilder()
to use the updated extension:
Widget _catWeightCommentBuilder() {
return Text(
_catWeightComment.firstLetterUppercase(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontStyle: FontStyle.italic,
),
);
}
Don’t forget to import the extension.
import '../utils/string_case_converter.dart';
_catWeightComment
will now start with an uppercase.
Hot reload to see that small change.
Generics
Like regular classes and methods, you can create Dart extensions on generic types. You’ll make one to insert an element between each original list element.
In the picture above, the original list contains numbers you would like to separate by a comma. This is what you want to achieve with your extension.
To do this on a generic List
, make an extension on List<T>
, where “T” is the type of the elements in the list.
First, create a file named separated_list.dart in lib/utils/, then paste the following code in it:
extension SeparatedList<T> on List<T> {
List<T> separated(T separator) {
final newList = <T>[];
for (var i = 0; i < length; i++) {
if (i == 0) {
newList.add(this[i]);
} else {
newList.add(separator);
newList.add(this[i]);
}
}
return newList;
}
}
The separated()
method adds a separator between each element of the original List
. Note that both the List
and the new element should be of type T
.
Here's an example of how to use it:
final myExampleList = <String>['Sam', 'John', 'Maya'];
print(myExampleList.separated(', ').join()); // Prints "Sam, John, Maya"
The ListView
widget has a separated
constructor like this.
You can now achieve something resembling it with Column
and Row
.
In lib/main.dart, locate the _mainColumnContent()
method. It returns the children of the main Column
of your widget tree. Note the space
variable at the method's beginning.
const space = SizedBox(height: 20);
It's used to add space among all the children of the Column
widget, which is the app's main structure. Delete that variable and all the lines where it appears.
Now, you need to use the new extension. Locate the comment TODO Add separation between items with an extension
and replace the entire line with the code below.
].separated(const SizedBox(height: 20));
With this code, you invoke separated()
on the widget list before returning it. The extension method inserts the SizedBox
between each original items.
Again, don't forget to import the extension.
import '../utils/separated_list.dart';
You can also make an extension method directly on List<Widget>
rather than on a generic List
. Paste the following code at the end of lib/utils/separated_list.dart:
extension SpacedWidgets on List<Widget> {
// 1.
// double defaultHorizontalSpace = 8;
// 2.
static const double _defaultHorizontalSpace = 8;
static const double _defaultVerticalSpace = 8;
// 3.
List<Widget> _spaced(
{required double horizontalSpace, required double verticalSpace}) {
// 4.
return separated(SizedBox(width: horizontalSpace, height: verticalSpace));
}
List<Widget> horizontallySpaced({
double horizontalSpace = _defaultHorizontalSpace,
}) {
return _spaced(horizontalSpace: horizontalSpace, verticalSpace: 0);
}
List<Widget> verticallySpaced({
double verticalSpace = _defaultVerticalSpace,
}) {
return _spaced(horizontalSpace: 0, verticalSpace: verticalSpace);
}
}
In the code above, you create an extension on a list of widgets. The extension defines a couple of methods that add space among the widgets in the list.
Some important limitations and features of Dart extensions are highlighted in the code:
- Declaring instance fields is not allowed.
- Implementing static fields is allowed.
- You can create private methods inside an extension.
- It's possible to reuse other extensions in an extension, like
SeparatedList
is used inSpacedWidgets
.
Remember to import the missing references.
import 'package:flutter/widgets.dart';
Thanks to SpacedWidgets
, you can now go back to lib/main.dart and replace your previous separated()
call with the new extension.
// Replace
].separated(const SizedBox(height: 20));
// with
].verticallySpaced(verticalSpace: 20);
You're now using SpacedWidgets
instead of SeparatedList
.
Private Dart Extensions
Like classes, you can make extensions private by starting their name with an _
.
To make SpacedWidgets
private, move it from lib/utils/separated_list.dart to main.dart because you'll use it only there, and rename it to _SpacedWidgets
:
extension _SpacedWidgets on List<Widget>{
// ...
}
Because it starts with an underscore, it's now private; you can only use it in the main.dart file.
You can also make extensions private by omitting their name:
extension on List<Widget>{
// ...
}
However, naming an extension make it easier to understand what it does. Moreover, it gives you an easier way to manage conflicts, as you'll see later.
Although it might sound good to make private extensions, you should identify where you can reuse them in your code and change them to be public. Extensions are helpful because they make code highly reusable.
Static Functions, Constructors and Factories
Dart extensions aren't yet perfect. They can't:
- Create new constructors
- Create factories
You can declare static functions like in the following example:
extension StringPrinter on String {
// 1.
// static String print() {
// print(this);
// }
// 2.
static String helloWorld() {
return 'Hello world';
}
}
Here's a breakdown of the code snippet above:
- You can't use
this
in a static method. That's because it's static: You make the call on the class, not on an instance of the class. - You can define a regular static method.
But its usage might disappoint you:
// Doesn't work
// String.helloWorld();
// Doesn't work
// 'something'.helloWorld();
// Works!
StringPrinter.helloWorld();
You can't use String
to call helloWorld()
. You have to use StringPrinter
directly, which isn't ideal. Being able to call String.helloWorld()
was the initial intention, after all.
For the CatFoodCalculator app, you might have liked to return a Slider
with a theme included in its constructor instead of having to wrap the Slider
with a SliderTheme
.
Copy the following code and paste it in a new file lib/utils/themed_slider.dart:
import 'package:flutter/material.dart';
extension ThemedSlider on Slider {
static Widget withTheme({
Key? key,
required double value,
required Function(double) onChanged,
Function(double)? onChangeStart,
Function(double)? onChangeEnd,
double min = 0.0,
double max = 1.0,
int? divisions,
String? label,
Color? activeColor,
Color? inactiveColor,
Color? thumbColor,
MouseCursor? mouseCursor,
String Function(double)? semanticFormatterCallback,
FocusNode? focusNode,
bool autofocus = false,
required SliderThemeData themeData,
}) {
return SliderTheme(
data: themeData,
child: Slider(
key: key,
value: value,
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
min: min,
max: max,
divisions: divisions,
label: label,
activeColor: activeColor,
inactiveColor: inactiveColor,
thumbColor: thumbColor,
mouseCursor: mouseCursor,
semanticFormatterCallback: semanticFormatterCallback,
focusNode: focusNode,
autofocus: autofocus,
),
);
}
}
The extension wraps the Slider
with a SliderTheme
instead of having to deal with it directly.
Now, in lib/main.dart, import the new file with:
import '../utils/themed_slider.dart';
Then, locate SliderTheme
, right below the // TODO Replace SliderTheme with ThemedSlider
comment. Replace SliderTheme
, the child of the Expanded
widget, with a call to the new extension as in the code below:
child: ThemedSlider.withTheme(
value: _mealRepartition,
min: 0,
max: _nbMeals.toDouble(),
divisions: _nbMeals,
onChanged: (newVal) {
setState(() {
_mealRepartition = newVal;
});
},
themeData: const SliderThemeData(
trackHeight: 16,
tickMarkShape: RoundSliderTickMarkShape(tickMarkRadius: 6),
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 16),
thumbColor: Color(0xffffa938),
),
You have to call ThemedSlider.withTheme()
instead of Slider.withTheme()
. This limitation is actively discussed in a GitHub issue.