Flutter:-Dart’s Awesome Extension Methods

What’s the problem that extension methods are trying to solve?

Imagine you’re using someone else’s library. For example, it’s a client library for Tesla, Inc. API, which gives us information about cars.

This library is awesome and accurate. But for my purposes, it’s a little hard to use. For example, it gives the cars average mileage in miles per charge. But all I really want is Km per charge.

Now we’re developers, so we know what to do

Make helper functions

So, we can wrap results from the library and get what we want. But that’s a lot of wrapping. I would personally much rather have these as members of the original class.

Extension Methods

In December 2019 the Dart language in its 2.7 version, received nice feature support: extension methods.

Extension methods allow you to add methods, getters, setters, and operators to classes, even classes which you don’t have any control of, for example: String. These are called statically, so it won’t work with dynamic variables, but it will work with Dart’s type inference.

You have to make sure that your Flutter project that uses the extension methods has to be ≥ 2.7

You can add functionality to core classes like int, duration, and string e.t.c

Syntax

extension <name (optional)> on <type> {  
 // methods, getters, setters, operators
}

Use this to reference the object the method is being called on.

Let’s take a look at some examples

Duration from number

extension DurationExtension on int {
  Duration get hours => Duration(hours: this);

  Duration get minutes => Duration(minutes: this);

  Duration get seconds => Duration(seconds: this);
}

void main() {
  print("10 minutes : ${10.minutes}");

  print("Seconds in 10 minutes : ${10.minutes.inSeconds}");

  var totalDuration = 1.hours + 30.minutes + 30.seconds;

  print("One Hour 30 minutes 30 seconds :$totalDuration");
}

Shared Preferences

Let’s get/set Color other than int, double, bool, string, and list of strings inSharedPreferences.

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

extension SPExtension on SharedPreferences {
  Color getColor(String key) {
    int value = this.getInt(key);
    return value == null ? null : Color(value);
  }

  setColor(String key, Color color) async => this.setInt(key, color?.value);
}

void myMethod() async {
  await sharedPreferences.setColor('primary_color', primaryColor);

  Color secondaryColor = sharedPreferences.getColor('secondary_color');
}

Parsing data from Map to a custom object

Suppose that you are getting this data from and API.

Map data = {
  'isAdult': false,
  'id': 0,
  'name': 'Ajay',
  'marks': 80,
  'knowLanguages': ['EN'],
};

Let’s convert it into a Person object.

class Person {
  bool isAdult;
  int id;
  String name;
  double marks;
  List<String> knowLanguages;

  Person({
    this.isAdult,
    this.id,
    this.name,
    this.marks,
    this.knowLanguages,
  });

  factory Person.fromMap(Map data) => Person(
        isAdult: data['isAdult'],
        id: data['id'],
        name: data['name'],
        marks: data['marks'],
        knowLanguages: List<String>.from(data['knowLanguages']),
      );

}

Let’s run some tests

Cool! it’s working But what happens if API doesn't give an expected response.

Let’s add some awesome extension methods to Map to avoid such scenarios.

These methods should take care of the below case

  • Map is null

  • Map has key

  • value is of incorrect type

  • value is null

const bool defaultBool = false;
const int defaultInt = 0;
const double defaultDouble = 0;
const String defaultString = '';

/// extension methods for Map
///
extension MapExtension on Map {
  /// Reads a [key] value of [bool] type from [Map].
  ///
  /// If value/map is NULL or not [bool] type return default value [defaultBool]
  ///
  bool getBool(String key) {
    Map data = this;
    if (data == null) {
      data = {};
    }
    if (data.containsKey(key)) if (data[key] is bool) return this[key] ?? defaultBool;
    print("Map.getBool[$key] has incorrect data : $this");
    return defaultBool;
  }

  /// Reads a [key] value of [int] type from [Map].
  ///
  /// If value/map  is NULL or not [int] type return default value [defaultInt]
  ///
  int getInt(String key) {
    Map data = this;
    if (data == null) {
      data = {};
    }
    if (data.containsKey(key)) return toInt(data[key]);
    print("Map.getInt[$key] has incorrect data : $this");
    return defaultInt;
  }

  /// Reads a [key] value of [double] type from [Map].
  ///
  /// If value/map  is NULL or not [double] type return default value [defaultDouble]
  ///
  double getDouble(String key) {
    Map data = this;
    if (data == null) {
      data = {};
    }
    if (data.containsKey(key)) return toDouble(data[key]);
    print("Map.getDouble[$key] has incorrect data : $this");
    return defaultDouble;
  }

  /// Reads a [key] value of [String] type from [Map].
  ///
  /// If value/map  is NULL or not [String] type return default value [defaultString]
  ///.
  String getString(String key) {
    Map data = this;
    if (data == null) {
      data = {};
    }
    if (data.containsKey(key)) if (data[key] is String) return data[key] ?? defaultString;
    print("Map.getString[$key] has incorrect data : $this");
    return defaultString;
  }

  /// Reads a [key] value of [List] type from [Map].
  ///
  /// If value/map  is NULL or not [List] type return default value [defaultString]
  ///
  List<T> getList<T>(String key) {
    Map data = this;
    if (data == null) {
      data = {};
    }
    if (data.containsKey(key)) if (data[key] is List<T>) return data[key] ?? <T>[];
    print("Map.getString[$key] has incorrect data : $this");

    return <T>[];
  }
}

///Parse an object to int if not valid returns 0
///
int toInt(Object value) {
  if (value != null) {
    try {
      int number = int.parse('$value');
      return number;
    } on Exception catch (e, s) {
      print("toInt Exception : $e\n$s");
    }
  }
  print("Error in toInt $value");
  return 0;
}

///Parse an object to double if not valid returns 0
///
double toDouble(Object value) {
  if (value != null) {
    try {
      double number = double.parse('$value') ?? 0.0;
      return number;
    } on Exception catch (e, s) {
      print("toDouble Exception : $e\n$s");
    }
  }
  print("Error in toDouble $value");
  return 0;
}

Let’s update the Person.fromMap

factory Person.fromMap(Map data) => Person(
      isAdult: data.getBool('isAdult'),
      id: data.getInt('id'),
      name: data.getString('name'),
      marks: data.getDouble('marks'),
      knowLanguages: List<String>.from(data.getList<String>('knowLanguages').map((e) => e).toList()),
    );

Now, let’s run some tests

That’s it no need to worry about any null exception while parsing data from Map.

Widgets

Column(
  children: [
    Padding(
      padding: EdgeInsets.all(8.0),
      child: Text("Nonstop"),
    ),
    Padding(
      padding: EdgeInsets.all(8.0),
      child: Text("IO"),
    ),
    Padding(
      padding: EdgeInsets.all(8.0),
      child: Text("Technologies Pvt Ltd"),
    ),
  ],
),

We can avoid nesting and also chain multiple calls without adding more indentations using extension methods.

extension WidgetExtension on Widget {
  Widget paddingAll(double padding) => Padding(padding: EdgeInsets.all(padding), child: this);
}Column(
  children: [
    Text("Nonstop").paddingAll(8),
    Text("IO").paddingAll(8),
    Text("Technologies Pvt Ltd").paddingAll(8),
  ],
)

I find this feature quite awesome and useful.

0
Subscribe to my newsletter

Read articles from NonStop io Technologies directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

NonStop io Technologies
NonStop io Technologies

Product Development as an Expertise Since 2015 Founded in August 2015, we are a USA-based Bespoke Engineering Studio providing Product Development as an Expertise. With 80+ satisfied clients worldwide, we serve startups and enterprises across San Francisco, Seattle, New York, London, Pune, Bangalore, Tokyo and other prominent technology hubs.