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?
Dart Extensions on Enums
Besides classes, you can also create extensions on enum
.
Open lib/widgets/meal_info.dart and note the MealType
enum declaration at the top of the file.
The amount of food you should feed to your cat depends on the specific food, and the package usually shows the recommended daily intake. One might not know where to find the correct information to type in this form. That's why there's a Help button, which displays a popup:
The popup content changes based on the MealType
. In your next extension, you'll create a method to show this popup.
Add an extension MealTypeDialog
in a new file, lib/utils/meal_type_dialog.dart:
import 'package:flutter/material.dart';
import '../widgets/meal_info.dart';
extension MealTypeDialog on MealType {
Future<void> infoPopup(BuildContext context) {
final text = this == MealType.wet
? 'You can find this data printed on the pack of wet food'
: 'Your bag of dry food should have this data printed on it';
return showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
content: Text(text),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
)
],
);
});
}
}
This extension displays the same dialog you get when you use the onInfoPressed()
method from _MealInfoState
. It shows a different text based on the MealType
.
In meal_info.dart, import the file with the new extension:
import '../utils/meal_type_dialog.dart';
Then, look for the // TODO Replace onInfoPressed with an extension
comment and replace the onPressed
with a call to the MealTypeDialog
extension.
onPressed: () => widget.mealType.infoPopup(context),
The infoPopup()
method now takes care of displaying the dialog. You don't need onInfoPressed()
anymore, so you can delete it.
And voilà! Thanks to your extension, you're now displaying a popup directly by calling a method on an enum
.
Handling Conflicts
The CatFoodCalculator app is quite simple: There's no API call nor local storage. If you'd like to implement it, converting your objects to JSON is a good starting point. One way of doing it is to use jsonEncode()
.
Create an extension JsonConverter
in a new file, lib/utils/json_converter.dart:
import 'dart:convert';
extension JsonConverter on dynamic {
// ...
}
You'll need dart:convert
because you'll use jsonEncode()
. Note that the extension is dynamic: It's available to all types, including your target class MealData
.
Now, add a new method to this extension:
String stringify() {
return jsonEncode(this);
}
As you can see, jsonEncode()
does the entire job.
In main.dart, find the // TODO add a save button here
comment and replace it with a Save button as in the code below.
List<Widget> _mainColumnContent() {
return [
...
ElevatedButton(
onPressed: _saveMealData,
child: const Text('SAVE'),
),
].verticallySpaced(verticalSpace: 20);
}
You'll use this button to simulate saving MealData
in _saveMealData()
. Create a new method in the _MyHomePageState
widget:
void _saveMealData() {
final mealData = MealData.dry(
nbMeals: _mealRepartition.round(),
eachAmount: _calculateRation(MealType.dry),
);
print('Json : ${mealData.stringify()}');
}
Import JsonConverter
extension:
import 'utils/json_converter.dart';
Instead of saving MealData
somewhere, you'll only print it to the console in this example, thanks to print()
. This is what you should read in the console:
{
"nbMeals": 3,
"mealType": "dry",
"eachAmount": 122
}
An alternative stringify
method could include the type of the object as the initial key:
{
"MealData":{
"nbMeals": 3,
"mealType": "dry",
"eachAmount": 122
}
}
Go back to json_converter.dart and create another extension:
extension JsonConverterAlt on dynamic {
String stringify() {
return '{$runtimeType: ${jsonEncode(this)}}';
}
}
This one includes the runtimeType as the first key.
Both JsonConverter
and JsonConverterAlt
have a method named stringify()
. In a real app, this might happen due to using an external library.
Go back to main.dart and note the error on stringify()
:
One way to solve it is to use the hide
feature in the import:
import 'utils/json_converter.dart' hide JsonConverterAlt;
The error disappears, but you can't use both extensions on main.dart with this method.
Another way to solve this problem is to use the names of your extensions: That's why you should name them. Remove the hide JsonConverterAlt
code you added to the import statement and replace the body of the _saveMealData()
method with the following:
final mealData = MealData.dry(
nbMeals: _mealRepartition.round(),
eachAmount: _calculateRation(MealType.dry),
);
print('Json v1 : ${JsonConverter(mealData).stringify()}');
print('Json v2 : ${JsonConverterAlt(mealData).stringify()}');
Wrapping your class with the extension helps to resolve conflicts when they occur simply, even if the API is a bit less fluid now.
Common Extension Usages
Now that you've learned what Dart extensions are and how to create them, it's time to see some common usages in real apps.
Adding Features to Classes
Extensions let you add features to existing Flutter and Dart classes without re-implementing them.
Here are a few examples:
- Convert a
Color
to a hexString
and vice versa. - Separating the children of a
ListView
using the sameWidget
as a separator in the entire app. - Convert a number of milliseconds from an
int
to a more humanly readableString
.
You can also add features to classes from external packages available at pub.dev.
People often put the code to add these features in Utils
classes such as StringUtils
. You might already have seen that in some projects, even in other languages.
Extensions provide a good alternative to them with a more fluid API. If you choose this approach, your StringUtils
code will become an extension instead of a class. Here are a few methods you could add to a StringUtils
extension:
String firstLetterUppercase()
bool isMail()
bool isLink()
bool isMultiline(int lineLength)
int occurrences(String pattern)
When writing a static method, consider whether an extension would work first. An extension might give you the same output but with a better API. That's especially nice when that method is useful in several places in your code. :]
Dart Extensions as Shortcuts
In Flutter, many widgets require the current BuildContext
, such as the Theme
and Navigator
. To use a TextStyle
defined in your Theme
within the build()
method of your widgets, you'll have to write something like this:
Theme.of(context).textTheme.headlineSmall
That's not short, and you might use it several times in your app. You can create extensions to make that kind of code shorter. Here are a few examples:
import 'package:flutter/material.dart';
extension ThemeShortcuts on BuildContext {
// 1.
TextTheme get textTheme => Theme.of(this).textTheme;
// 2.
TextStyle? get headlineSmall => textTheme.headlineSmall;
// 3.
Color? get primaryColor => Theme.of(this).primaryColor;
}
Here's a breakdown of the code above:
- You make the
textTheme
more easily accessible:
// Without extension
Theme.of(context).textTheme
// With extension
context.textTheme
- Use your previous
textTheme
method to return aTextStyle
. The code is clearly shorter:
// Without extension
Theme.of(context).textTheme.headlineSmall
// With extension
context.headlineSmall
- You can add as many methods as you'd like to make shortcuts, such as to get the
primaryColor
:
// Without extension
Theme.of(this).primaryColor
// With extension
context.primaryColor