Dart null / false / empty checking: How to write this shorter?

This is my code for true on everything but empty string, null and false:

if (routeinfo["no_route"] == "" || routeinfo["no_route"] == null || routeinfo["no_route"] == false) {
    // do sth ...
}

This is my code for true on everything but empty string, null, false or zero:

if (routeinfo["no_route"] == "" || routeinfo["no_route"] == null || routeinfo["no_route"] == false || routeinfo["no_route"] == 0) {
    // do sth...
}

How can I write this shorter in Dart? Or is it not possible?


Solution 1:

If your requirement was simply empty or null (like mine when I saw this title in a search result), you can use Dart's safe navigation operator to make it a bit more terse:

if (routeinfo["no_route"]?.isEmpty ?? true) {
  // 
}

Where

  • isEmpty checks for an empty String, but if routeinfo is null you can't call isEmpty on null, so we check for null with
  • ?. safe navigation operator which will only call isEmpty when the object is not null and produce null otherwise. So we just need to check for null with
  • ?? null coalescing operator

If your map is a nullable type then you have to safely navigate that:

if (routeinfo?["no_route"]?.isEmpty ?? true) {
  //
}

Solution 2:

You could do

if (["", null, false, 0].contains(routeinfo["no_route"])) {
  // do sth
}

Solution 3:

Late 2020 Update

Summary

  • This answer holds true, except for isNull and isNotNull. They no longer provide type promotion when Null Safety is introduced in dart/flutter in the future.
  • Other helpers like isNullOrEmpty do not provide type promotion, as they are in a different (sub-)scope compared to callsite.
  • My personal opinion, is that you can drop isNull and isNotNull but keep other helpers as you shouldn't expect them to do type promotion for you.

Explanation

  • This is because Null Safety was finally introduced in Dart/Flutter. But as of October 2020, this feature is still not available for stable releases on dart/flutter. Check out the quick guide Null Safety or the thorough Understanding Null Safety.
  • Null Safety in Dart/Flutter should be similar to Swift (Optional) and Kotlin (Nullable Types, Non-Nullable Types).

Demonstration

Here's a demonstration why encapsulation/helper-getter of isNull (== null) and isNotNull (!= null) is a very big problem:

// Promotion works
int definitelyInt(int? aNullableInt) {
  if (aNullableInt == null) { // Promote variable `aNullableInt` of Nullable type `int?` to Non-Nullable type `int`
    return 0;
  }
  return aNullableInt; // Can't be null! This variable is promoted to non-nullable type `int`
}

When "Null Safety" is shipped in the dart release you are using, the type promotion in above code works! HOWEVER:

// Promotion does NOT work!!!
int definitelyInt(int? aNullableInt) {
  if (aNullableInt.isNull) { // does NOT promote variable `aNullableInt` of Nullable type `int?`
    return 0;
  }
  return aNullableInt; // This variable is still of type `int?`!!!
}

The above doesn't work, because the null check == null and != null are encapsulated in a sub-scope (different stack frame) and not inferred to have this "promotion" effect within definitelyInt scope.



Early 2020 Update

Here's a modified version using getters instead of instance/class methods and covering whitespaces, isNull, and isNotNull.

Second Update

It also accounts for empty lists and maps now!

Third Update

Added private getters for safety and modularity/maintainability.

Fourth Update

Updated the answer to fix a particular problem with Map, it's not correctly being handled when empty! Because Map is not an Iterable. Solved this issue by introducing _isMapObjectEmpty U

Solution

extension TextUtilsStringExtension on String {
  /// Returns true if string is:
  /// - null
  /// - empty
  /// - whitespace string.
  ///
  /// Characters considered "whitespace" are listed [here](https://stackoverflow.com/a/59826129/10830091).
  bool get isNullEmptyOrWhitespace =>
      this == null || this.isEmpty || this.trim().isEmpty;
}

/// - [isNullOrEmpty], [isNullEmptyOrFalse], [isNullEmptyZeroOrFalse] are from [this StackOverflow answer](https://stackoverflow.com/a/59826129/10830091)
extension GeneralUtilsObjectExtension on Object {
  /// Returns true if object is:
  /// - null `Object`
  bool get isNull => this == null;

  /// Returns true if object is NOT:
  /// - null `Object`
  bool get isNotNull => this != null;

  /// Returns true if object is:
  /// - null `Object`
  /// - empty `String`s
  /// - empty `Iterable` (list, set, ...)
  /// - empty `Map`
  bool get isNullOrEmpty =>
      isNull ||
      _isStringObjectEmpty ||
      _isIterableObjectEmpty ||
      _isMapObjectEmpty;

  /// Returns true if object is:
  /// - null `Object`
  /// - empty `String`
  /// - empty `Iterable` (list, map, set, ...)
  /// - false `bool`
  bool get isNullEmptyOrFalse =>
      isNull ||
      _isStringObjectEmpty ||
      _isIterableObjectEmpty ||
      _isMapObjectEmpty ||
      _isBoolObjectFalse;

  /// Returns true if object is:
  /// - null `Object`
  /// - empty `String`
  /// - empty `Iterable` (list, map, set, ...)
  /// - false `bool`
  /// - zero `num`
  bool get isNullEmptyFalseOrZero =>
      isNull ||
      _isStringObjectEmpty ||
      _isIterableObjectEmpty ||
      _isMapObjectEmpty ||
      _isBoolObjectFalse ||
      _isNumObjectZero;

  // ------- PRIVATE EXTENSION HELPERS -------
  /// **Private helper**
  ///
  /// If `String` object, return String's method `isEmpty`
  ///
  /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `String`
  bool get _isStringObjectEmpty =>
      (this is String) ? (this as String).isEmpty : false;

  /// **Private helper**
  ///
  /// If `Iterable` object, return Iterable's method `isEmpty`
  ///
  /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `Iterable`
  bool get _isIterableObjectEmpty =>
      (this is Iterable) ? (this as Iterable).isEmpty : false;

  /// **Private helper**
  ///
  /// If `Map` object, return Map's method `isEmpty`
  ///
  /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `Map`
  bool get _isMapObjectEmpty => (this is Map) ? (this as Map).isEmpty : false;

  /// **Private helper**
  ///
  /// If `bool` object, return `isFalse` expression
  ///
  /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `bool`
  bool get _isBoolObjectFalse =>
      (this is bool) ? (this as bool) == false : false;

  /// **Private helper**
  ///
  /// If `num` object, return `isZero` expression
  ///
  /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `num`
  bool get _isNumObjectZero => (this is num) ? (this as num) == 0 : false;
}

Assumptions

This presumes Dart 2.7 or above to support Extension Methods.

Usage

The above are two Extension classes. One for Object and one for String. Put them in a file separately/together and import the files/file at callsite file.

Description

Coming from Swift, I tend to use extensions like user @Benjamin Menrad's answer but with getter instead of method/function when applicable -- mirroring Dart's computed properties like String.isEmpty.

Coming from C#, I like their String's helper method IsNullOrWhiteSpace.

My version merges the above two concepts.

Naming

Rename the Extension classes whatever you like. I tend to name them like this:

XYExtension

Where:

  • X is File/Author/App/Unique name.
  • Y is name of Type being extended.

Name examples:

  • MyAppIntExtension
  • DartDoubleExtension
  • TextUtilsStringExtension
  • OHProviderExtension

Criticism

Why private getters instead of simple this == null || this == '' || this == [] || this == 0 || !this

  • I think this is safer as it first casts the object to the right type we want to compare its value to.
  • More modular as changes are central within the private getters and any modification is reflected everywhere.
  • If type check fails within the private getter, we return false which doesn't affect the outcome of the root logical-OR expression.