Dartx and the Power of Extensions— How it’s Made

christiannitas
8 min readMay 15, 2023

--

Dartx is one of my must-have dart packages on pub.dev which through the powerful use of extensions simplifies the way we write common logic for list processing, string handling, and date-time manipulations.

You can read more about it in one of my previous articles on the topic where I showcase some of the most interesting things you can do with it.

In this article, however, I want to dive deeper into how this package is implemented and explore the simple dart concepts behind its seemingly complex extension methods.

Photo by Arif Riyanto on Unsplash

Extensions in Dart

Ever since Dart 2.7, extensions have been a game changer for writing Dart & Flutter code. In short, with extensions, you can add extra functionality to types and classes which you do not control. This functionality is not only limited to implementing functions. You can also add getters and setters and even overload operators on said types.

This is useful when you want to extend types and classes from the standard dart library to have extra functionality. One of my favorite examples is the time library on pub.dev, which overloads arithmetic operators to make operations with time values a breeze. You can check it out here:

This is also what constitutes the core functionality of the Dartx package, which adds extra logic to standard library types, like iterators, strings, and integers.

Let’s look at the most basic example of a dart extension. We will create an extension on the String type called toIntOrNull() which when called will try to parse its contents and return either an integer or null.

extension ToIntOrNull on String {
int? toIntOrNull() {
return int.tryParse(this);
}
}

To explain the previous code snippet, we declare an extension and we give it a name. We then specify the type we are trying to extend using the on keyword. Afterward, we can begin writing our functions, operator overloads, getters, and setters. Notice that when writing the function we do not need to pass any parameter, as this function will be called on a String object. In the scope of the extension, this keyword refers to the object on which this function was called, and will be always of the type defined in the extension declaration, in this case, String.

Once this extension is defined, we can use it on any string object or string literal in our code, by importing the file it is declared in:

void main() {
print("203".toIntOrNull()); // 203
print("not a number".toIntOrNull()); // null
}

Dartx Extensions up close

As explained in my previous article, Dartx is a powerful package on pub.dev that exposes a number of extensions on types such as Lists, Iterables, Strings, integers, floats, and other base types from the standard dart library.

In the following section, we will look at some of the more interesting extensions and how they are implemented. We will only look at some examples, but you can feel free to take the information in this article and go look for yourself in the source code of Dartx available on GitHub. I really recommend installing it and trying it out for yourself. It’s worth it!

Extensions on String

One of the many extensions on strings was implemented in the very example I have given in the previous sections, however, there are many more available. Let us say you want to create an extension to return the reverse of the string to, for example, check if the string is a palindrome. We can do that by creating an extension and using the StringBuffer to build the new string.

extension ReversedString on String {
String get reversed {
StringBuffer buffer = StringBuffer();

for (int i = length - 1; i >= 0; i--) {
buffer.write(this[i]);
}

return buffer.toString();
}
}

In this example, I have created a ReversedString extension on String which implements the getter reversed just like in Dartx. However, their implementation uses an iterator to go through the contents of the string. Here I just use plain indexing on the string object by using this[i]. Also, notice how calling the length getter is used like a class member in this function. That is because all extension methods have access to the members of the extended class.

Now we can just call these methods as so:

print("stressed".reversed); // desserts 😋

And this is how you implement this method. However, you don’t have to, because you can just use Dartx and its many extension methods to do this!

Extensions on Lists & Iterables

When creating extensions on lists and iterables, but especially on the latter, it is usually a good practice to use lazily evaluated generator functions.

Generators are special functions in Dart that return an iterable. They are declared using the sync* keyword (similar to the way you would declare an asynchronous function or a stream generator, which is quite similar to normal generators). But what is so special about them?

Well, when using generators, instead of ending the execution and returning the value using the return keyword, you output a value using the yield keyword. But what’s the difference? Well, up to right now not a lot. However, the difference comes when you want to iterate over it. Because when you will try to get the next element, the execution of the function will begin right after the yield statement. Let’s take an example:

Iterable<int> createIterable(int max) sync* {
for (int i = 0; i < max; i++) {
yield i;
print("yielded $i");
}
}

void main() {
final iterable = createIterable(10);

for (final i in iterable) {
print(i);
}
}

In this snippet, we create a generator function that will give us an iterable of ints from 0 to max. If we try to go over this iterable using a for, we will print out values from 0 to max. If you run this, you will notice that the “yielded $i” message will always come after the print(i) in the main loop. That is because once you yield a value if you request the next value (in this case requesting is done by the for in the background) the generator will begin execution right after the last yield. We can also convert this iterable to a list.

final iterable = createIterable(10);

print(List.from(iterable)); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In this case, the list constructor will repeatedly call the generator function and “consume” the values generated by appending them to the list.

Iterables are a difficult concept to wrap your head around, but once you do you can use them to create efficient generators and list processors. One of the big advantages of iterables is that they are lazily generated. If let’s say you only need half of the values, you can just not request the next ones and they will not get computed. This is not useful in our example, but when the generation of these numbers is costly, then it would be helpful to not perform costly work. You can watch this amazing video from the Flutter official youtube channel to get a better grasp on generator functions:

Flutter In Focus series explaining Generator Functions

Now, let’s see how we can use these generator functions to implement the cycle() function from Dartx. The cycle function is called on a list and will generate a new iterable which will cycle over that list indefinitely.

extension Cycler<E> on Iterable<E> {
Iterable<E> cycle() sync* {
var it = iterator;
if (!it.moveNext()) return;

yield it.current;
while (true) {
while (it.moveNext()) {
yield it.current;
}
it = iterator;
}
}
}

void main() {
final iterable = [1, 2, 3, 4].cycle();

for (int i in iterable) {
print(i); // 1 2 3 4 1 2 3 4 1 2 ... forever
}
}

To implement that function, we will make use of the iterator object of the iterable we extend. iterator is a getter on the iterable which returns an object with two important members. The current member will give you the current value of the iterator, firstly 1. When we call moveNext() the current value of the iterator becomes the next value in the list, 2 in this case, and so on…

The function is implemented by simply moving with the iterator through the iterable and yielding the values. Once we reach the end of the list (moveNext() returns false) we will reinitialize the it object with the iterator of the iterable and begin again.

Note: If we call such a generator in a for loop, we will continuously get values forever, because of the infinite loop in the generator function. However, the generator returns an iterable which again can be iterated through using an iterator, thus we could use the values and then stop at any time we want. Remember, the generator function will run only when we request another value from it. Otherwise, it will not.

Extensions on Datetime

Using Dartx will automatically import the time.dart package, which is an amazing set of utility functions that simplify working with dates. These extensions overload the operators on Datetime so we may add and subtract durations and get the resulting DateTime objects. This is how they did it:

extension AddSub on DateTime {
DateTime operator +(Duration duration) => add(duration);

DateTime operator -(Duration duration) => subtract(duration);
}

void main() {
DateTime a = DateTime.now();

print(a); // 2023-05-01 13:30:06
print(a + Duration(hours: 1)); // 2023-05-01 14:30:06
}

Note how we used the keyword operator followed by the operator itself to declare an overload on the operator. The return type is the result of the addition, while the argument is the other operand. (in this case Duration(hours:1))

You can see that the code is quite simple because Dart already implements functions to safely add/subtract duration to a DateTime, however, by overloading operators in the extension, we create a more straightforward way of adding and subtracting time.

You could say that it is more unintuitive like this, which is a fair criticism, especially if coming from the Java world. However, as you try to perform more and more complex operations on dates, you begin to see the drawbacks of using functions. The code becomes jumbled and hard to read. While with this, we can take advantage of the mathematical operation evaluation order and, once we know the meaning of the operators, we can tell more quickly what the code is trying to achieve.

Conclusion

Dartx and Time.dart are two packages that expose a set of extensions that simplify the way we write code and make it more readable. As we have seen, these complex functions were actually implemented using some of the base functionalities of the Dart programming language and anyone can implement them if they need to, however, why do that when you have this amazing package?

🔥 If you enjoyed this article and want to support such a style of deep dives into Flutter & Dart packages and learn more of the things that are going on behind the scenes, please consider following me on Medium and hitting the 👏 button. ‘Tis greatly appreciated. Cheers!

--

--

christiannitas
christiannitas

Written by christiannitas

Software Developer, passionate and writing about mobile, web, backend and anything I find interesting.

No responses yet