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 2 of 4 of this article. Click here to view the first page.

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.

The comment text is updated by using Dart extensions on a nullable type

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.

Add a separator between each element of a list

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:

  1. Declaring instance fields is not allowed.
  2. Implementing static fields is allowed.
  3. You can create private methods inside an extension.
  4. It's possible to reuse other extensions in an extension, like SeparatedList is used in SpacedWidgets.

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:

  1. 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.
  2. 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.

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.