Lesser known classes and functions from the Dart core libraries

I noticed how, even after a decade of using Dart, I still occasionally find functionality in the standard library that’s either new to me, or long forgotten. I’d like to invite you folks to share your favorite lesser known classes and functions from the standard library and other libraries maintained by the Dart team (like package:async, package:collection etc.).

I’ll start:

minBy() and maxBy() from package:collection

Usage:

var cars = <Car>[...];
var fastest = maxBy(cars, (c) => c.speed);

Obviously much faster than sorting the whole collection and taking the first or last element, assuming you really only need the one maximum or minimum value. I used to lug around a function that did this until I (re-)discovered this gem.

48 Likes

Recently, I learned about the nonNulls extension on the Iterable<T>. Using it everywhere where:

  • Any non-null filtering is needed - a fundamental usage, I suppose
final names = ['John', null, 'Steve'];
final nonNullNames = names.nonNulls; // ['John', 'Steve']
  • Where you have a list of widgets and want to add some spacing between them, thus you cannot really use SizedBox.shrink() as a placeholder. Now, using null in these situations and using the nonNulls on the list - feels good man.
// A custom extension that adds a separator Widget between elements
extension IterableExtension<T> on Iterable<T> {
  List<T> addBetween(T separator) {
    return expand((item) sync* {
      yield separator;
      yield item;
    }).skip(1).toList();
  }
}

Column(
  children: <Widget?>[
    Text('Hey'),
    _customBuilder(), // Some builder that can return null
    Text('You')
  ]
    .nonNulls
    .addBetween(SizedBox(height: 8))
    .toList(),
),
25 Likes

I think in general the package:collection package is a bit of a gem. I often find myself implementing some list operations but later discover something similar in collection. Maybe it would be a bit more discoverable if the autocomplete was aware of all the extensions in this package by default.

11 Likes

With the next release you’ll be able to just use the spacing argument instead. :smiley:

Column(
  children: <Widget?>[
    Text('Hey'),
    _customBuilder(), // Some builder that can return null
    Text('You')
  ].nonNulls,
  spacing: 8,
),
20 Likes

nonNulls is handy, but nonNulls.toList() there looks a little ugly, so I would use if-case instead. It’s just a matter of preference.

children: [
  Text('Hey'),
  if (_customBuilder() case final customWidget?) customWidget,
  Text('You')
]
7 Likes

I know right! Looking forward to it :sweat_smile:

1 Like

Maybe it’s just not the right example, but if you are building your Widget list as an iterable, that will be needed anyway.

3 Likes

AsyncCache.ephemeral from package:async

An ephemeral cache guarantees that a callback function will only be executed at most once concurrently.

9 Likes

Would you mind sharing a bit more about when this would be useful? I’ve read the linked docs but some more concrete examples would help.

3 Likes

Great tip regarding nonNulls!

It’s funny for me to see this including in this post as I actually encountered this very same problem recently where I was getting unexpected null values in my List from an external API and needed a solution.

I used whereType instead of nonNulls.

This is more type safe when you are working with Iterable<dynamic> and have no guarantee that the data source you are using will definitely be either String or null.

final names = ['John', null, 'Steve'];
final nonNullNames = names.whereType<String>(); // ['John', 'Steve']
5 Likes
Future<void> asyncOperation() async {
  await Future<void>.delayed(const Duration(seconds: 2));
  print('Executed');
}
final AsyncCache _cache = AsyncCache.ephemeral();

@override
Widget build(BuildContext context) {
  return ElevatedButton(
    onPressed: () => _cache.fetch(asyncOperation),
    child: const Text('Execute'),
  );
}

Without fetch(), the asyncOperation function is executed as many times as you press the button while the operation is still ongoing, so “Executed” is printed many times. You can make sure It won’t happen by using fetch().

If the operation can take long, it would be better to disable the button. If disabling the button doesn’t seem like the right way because you know the operation always completes in an instant, but you still need to prevent button mashing, AsyncCache.ephemeral might be an easier solution.

23 Likes

Really cool. Thanks for sharing :slight_smile:

1 Like

For me, it’s firstWhereOrNull. I used it when the orElse function on the Iterative class requires a non-null return value.

MyStuff? stuff = someStuffs.firstWhereOrNull((element) => element.id == 'Cat');
3 Likes

One trick I use when decoding http responses is to not parse the long string into Map in the main isolate, but rather:

  • spawn a new isolate
  • and use Utf8Decoder fused with JsonDecoder
Future<Map<String, dynamic>?> isolateHttpResponseDecoder(
  http.Response httpResponse,
) async =>
    Isolate.run(
      () => const Utf8Decoder().fuse(const JsonDecoder()).convert(httpResponse.bodyBytes) as Map<String, dynamic>?,
    );

I have found this and similar recommendations across github and some other communities. @mraleph (x.com) also mentioned this in his talk Learning something about Dart performance by optimizing jsonDecode.

I tested this couple months ago and the performance was greatly improved compared to just jsonDecode().

Edit: here is a bit of Dart SDK logic that in case of the above fuse expression creates a specialized _JsonUtf8Decoder.

9 Likes

Interesting! So, putting parallel execution in Isolate.run() aside for the moment, simply using const Utf8Decoder().fuse(const JsonDecoder()).convert(httpResponse.bodyBytes) as Map<String, dynamic>? is faster than jsonDecode()? Is this explained somewhere?

I think this is the umbrella ticket for the JSON decoder performance issues, but some discussions about it happened across various http/graphql repositories (e.g. ferry/gql had some performance improvements, one mention here)

1 Like

did you make some experiments from which size of json response this makes sense?

In our case the UX improvements (i.e. no dropped frames) could be noticed with 300-500 entries in a json (each including about 10 string/num properties). I don’t have exact numbers as this was over a year ago, but these 2 steps (separate isolate and fused decoder) significantly improved the first few seconds of the app experience to the point I didn’t have to worry about splitting the fetching into number of pages. Not sure which one of the above has a greater effect too.

3 Likes

I migrated Dio to use this approach, and the improvement was pretty significant, see x.com

4 Likes

Late to the party, but yes this 100%. I feel this should be the default behaviour rather than firstWhere which just throws if there’s no element found, surely no one wants that behaviour?

2 Likes