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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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 popups giving more information on how much food a cat should eat

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():

Note: A member named 'stringify' is defined in extension 'JsonConverter' and extension 'JsonConverterAlt', and none is more specific.

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 hex String and vice versa.
  • Separating the children of a ListView using the same Widget as a separator in the entire app.
  • Convert a number of milliseconds from an int to a more humanly readable String.

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:

  1. You make the textTheme more easily accessible:
// Without extension
Theme.of(context).textTheme
// With extension
context.textTheme
  1. Use your previous textTheme method to return a TextStyle. The code is clearly shorter:
// Without extension
Theme.of(context).textTheme.headlineSmall
// With extension
context.headlineSmall
  1. 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
Sébastien Bel

Contributors

Sébastien Bel

Author

Michele Volpato

Tech Editor

Adriana Kutenko

Illustrator

Aldo Olivares

Final Pass Editor

Brian Moakley

Team Lead

Over 300 content creators. Join our team.