Operators and Expressions in Dart: Arithmetic, Logical, Relational, and Bitwise Operators

Operators and Expressions in Dart

When I first started writing Dart code, I treated operators like background noise — the +, -, &&, and == symbols I’d used in every other language. It didn’t take long before I realized that Dart has its own personality when it comes to operators. Things like null-aware operators, the cascade operator, and integer-based bitwise math on a language that also runs in a browser (where JavaScript numbers behave differently) all forced me to slow down and actually understand what was happening under the hood. In this article, I’m going to walk you through everything I’ve learned about operators and expressions in Dart — from the basics you’ll use on day one to the internal details that explain why things behave the way they do.

What Is an Expression in Dart?

Before diving into operators, I want to be precise about terminology, because it matters more in Dart than people expect.

An expression is any piece of code that evaluates to a value. 2 + 3, isActive && isVerified, and even a function call like getUserName() are all expressions because they produce a result.

A statement, on the other hand, performs an action but doesn’t necessarily return a value — think of an if block or a for loop.

Dart blurs this line a bit more than some languages because it supports expression-bodied functions and even an if-else expression-like construct in newer versions (via conditional expressions). Here’s a simple comparison:

// Statement
void printSum(int a, int b) {
  print(a + b);
}

// Expression-bodied function (still a statement at the function level, but the body is an expression)
int sum(int a, int b) => a + b;

void main() {
  print(sum(4, 5)); // Output: 9
}

I mention this distinction because operators are the building blocks of expressions, and understanding that helps when you start chaining them together in real applications.

Arithmetic Operators in Dart

Arithmetic operators are the ones I use the most, especially when working with data-heavy Flutter apps that calculate prices, positions, or animations.

OperatorMeaningExampleResult
+Addition5 + 38
-Subtraction5 - 32
*Multiplication5 * 315
/Division (always returns double)5 / 22.5
~/Integer (truncating) division5 ~/ 22
%Modulo (remainder)5 % 21
-exprUnary minus-5-5
++ / --Increment / Decrementi++increments i by 1

Here’s a working example that I like to use when teaching this concept, because it shows a subtle behavior beginners often miss:

void main() {
  int a = 10;
  int b = 3;

  print(a + b);   // 13
  print(a - b);   // 7
  print(a * b);   // 30
  print(a / b);   // 3.3333333333333335  <-- always a double
  print(a ~/ b);  // 3  <-- integer division
  print(a % b);   // 1  <-- remainder

  double x = 7.5;
  print(x ~/ 2);  // 3 (still works on doubles, returns an int-like double truncation)
}

Why does / always return a double, even with two integers? This trips a lot of people up coming from languages like Java or C, where int / int gives you an int. In Dart, the / operator is defined on the num type to always produce a double, specifically to avoid the classic “why did my division silently truncate” bug. If you want integer behavior, Dart forces you to be explicit with ~/. I actually appreciate this design decision — it removes an entire category of subtle arithmetic bugs from my code.

Increment and Decrement: Prefix vs Postfix

void main() {
  int counter = 5;

  print(counter++); // prints 5, THEN increments to 6
  print(counter);   // 6

  print(++counter); // increments to 7, THEN prints 7
  print(counter);   // 7
}

I always tell newer developers: if you’re not 100% sure which form to use inside a complex expression (like array[i++] = array[++j]), just split it into two lines. Clarity beats cleverness, especially in production Flutter code that other people will maintain.

Relational (Comparison) Operators

Relational operators compare two values and always evaluate to a bool.

OperatorMeaning
==Equal to
!=Not equal to
>Greater than
<Less than
>=Greater than or equal to
<=Less than or equal to
void main() {
  int score = 85;

  print(score == 85);  // true
  print(score != 90);  // true
  print(score > 90);   // false
  print(score >= 85);  // true
  print(score <= 100); // true
}

== and Object Equality — the Part People Get Wrong

Here’s something that genuinely confused me early on: == in Dart doesn’t always mean “same object in memory.” For primitive types like int, double, String, and bool, == compares values. But for custom classes, == compares object identity by default — unless you override it.

class Point {
  final int x;
  final int y;
  Point(this.x, this.y);
}

void main() {
  var p1 = Point(2, 3);
  var p2 = Point(2, 3);

  print(p1 == p2); // false! Different instances in memory
}

To make p1 == p2 return true, I need to override == and hashCode:

class Point {
  final int x;
  final int y;
  Point(this.x, this.y);

  @override
  bool operator ==(Object other) =>
      other is Point && other.x == x && other.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  var p1 = Point(2, 3);
  var p2 = Point(2, 3);
  print(p1 == p2); // true now
}

This matters a lot in Flutter, where widgets and state objects often need value-based equality checks to work correctly with setState, lists, and Set/Map collections.

Logical Operators

Logical operators combine boolean expressions.

OperatorMeaning
&&Logical AND
||Logical OR
!Logical NOT
void main() {
  bool isLoggedIn = true;
  bool isAdmin = false;

  print(isLoggedIn && isAdmin); // false
  print(isLoggedIn || isAdmin); // true
  print(!isLoggedIn);           // false
}

Short-Circuit Evaluation

Dart’s && and || are short-circuiting, meaning the second operand isn’t evaluated if the result is already determined by the first. I rely on this constantly to avoid null errors:

void main() {
  String? name;

  if (name != null && name.length > 3) {
    print('Valid name');
  } else {
    print('Invalid or missing name');
  }
}

If name is null, Dart never evaluates name.length, so there’s no risk of a null-reference crash. This pattern is everywhere in real Dart and Flutter codebases, especially before Dart’s sound null safety became the default.

Bitwise and Shift Operators

Bitwise operators work directly on the binary representation of integers. I use these far less often in day-to-day Flutter UI work, but they show up constantly in things like flags, permission systems, color manipulation (ARGB values), and low-level performance-sensitive code.

OperatorMeaning
&Bitwise AND
|Bitwise OR
^Bitwise XOR
~Bitwise NOT (complement)
<<Left shift
>>Right shift
>>>Unsigned (logical) right shift
void main() {
  int a = 6;  // binary: 0110
  int b = 3;  // binary: 0011

  print(a & b);  // 0010 -> 2
  print(a | b);  // 0111 -> 7
  print(a ^ b);  // 0101 -> 5
  print(~a);     // -7 (two's complement)
  print(a << 1); // 1100 -> 12
  print(a >> 1); // 0011 -> 3
}

A real-world example I use often: extracting color channels from a 32-bit ARGB integer, which is exactly how Flutter’s Color class works internally.

void main() {
  int colorValue = 0xFF6200EE; // a purple color in ARGB

  int alpha = (colorValue >> 24) & 0xFF;
  int red   = (colorValue >> 16) & 0xFF;
  int green = (colorValue >> 8) & 0xFF;
  int blue  = colorValue & 0xFF;

  print('A:$alpha R:$red G:$green B:$blue');
  // Output: A:255 R:98 G:0 B:238
}

This is genuinely how Color.alpha, Color.red, Color.green, and Color.blue are implemented internally in Flutter, so understanding bitwise operators isn’t just academic — it explains real framework code you’ll read.

Assignment Operators

Dart supports compound assignment operators that combine an operation with assignment:

void main() {
  int total = 10;

  total += 5;  // total = total + 5
  total -= 2;  // total = total - 2
  total *= 3;  // total = total * 3
  total ~/= 4; // total = total ~/ 4

  print(total); // 12
}

Null-Aware Operators: Dart’s Signature Feature

If there’s one category of operators that feels distinctly “Dart” to me, it’s the null-aware family. These exist because of Dart’s sound null safety system, and they let me write defensive code without a wall of if (x != null) checks.

OperatorMeaning
??If left is null, use right
??=Assign right only if left is null
?.Access member only if left isn’t null
!Assert the value is not null (throws if it is)
void main() {
  String? username;

  String displayName = username ?? 'Guest';
  print(displayName); // Guest

  username ??= 'NewUser';
  print(username); // NewUser

  int? length = username?.length;
  print(length); // 8

  String forcedName = username!;
  print(forcedName); // NewUser
}

I want to be honest about the ! (null assertion) operator: I use it sparingly. It essentially tells the Dart compiler “trust me, this isn’t null,” and if I’m wrong, the app throws a runtime exception. In production Flutter apps, I prefer ?? with a sensible fallback over ! whenever possible, because it fails gracefully instead of crashing.

The Cascade Operator (.. and ?..)

The cascade notation is one of Dart’s more unusual features, and it took me a while to appreciate it. It lets you perform a sequence of operations on the same object without repeating its name.

class Robot {
  String? name;
  int power = 0;

  void greet() => print('Hi, I am $name with power $power');
}

void main() {
  var robot = Robot()
    ..name = 'Optimus'
    ..power = 100
    ..greet();
}

This compiles to a series of statements on the same object reference, but reads far more fluently — especially when configuring widgets, controllers, or builder-pattern objects in Flutter.

The Spread Operator (... and ...?)

Spread operators let you insert all elements of a collection into another collection literal.

void main() {
  List<int> baseNumbers = [1, 2, 3];
  List<int> extended = [0, ...baseNumbers, 4, 5];

  print(extended); // [0, 1, 2, 3, 4, 5]

  List<int>? maybeNull;
  List<int> safeExtended = [0, ...?maybeNull, 4];
  print(safeExtended); // [0, 4] — no crash even though maybeNull is null
}

I use this heavily when building dynamic widget lists in Flutter, such as conditionally including extra children in a Column.

Operator Precedence: Why Order Matters

Dart evaluates expressions according to a strict precedence table, from highest to lowest priority (simplified view):

  1. Unary postfix (++, --, ., ?., !)
  2. Unary prefix (-, !, ~, ++, --)
  3. Multiplicative (*, /, %, ~/)
  4. Additive (+, -)
  5. Shift (<<, >>, >>>)
  6. Bitwise AND (&)
  7. Bitwise XOR (^)
  8. Bitwise OR (|)
  9. Relational (<, >, <=, >=)
  10. Equality (==, !=)
  11. Logical AND (&&)
  12. Logical OR (||)
  13. If-null (??)
  14. Conditional (?:)
  15. Assignment (=, +=, etc.)
void main() {
  int result = 2 + 3 * 4;      // multiplication first: 2 + 12 = 14
  bool check = 5 > 3 && 2 < 4; // relational first, then &&
  print(result); // 14
  print(check);  // true
}

Whenever precedence isn’t immediately obvious to me — and even when it is — I add parentheses. It costs nothing at runtime and saves a huge amount of time during code review.

Overriding Operators on Custom Classes

Dart lets you define custom operator behavior for your own classes, which is genuinely powerful for things like vector math, money calculations, or custom collection types.

class Vector2D {
  final double x, y;
  Vector2D(this.x, this.y);

  Vector2D operator +(Vector2D other) => Vector2D(x + other.x, y + other.y);
  Vector2D operator *(double scalar) => Vector2D(x * scalar, y * scalar);

  @override
  String toString() => 'Vector2D($x, $y)';
}

void main() {
  var v1 = Vector2D(1, 2);
  var v2 = Vector2D(3, 4);

  print(v1 + v2);   // Vector2D(4.0, 6.0)
  print(v1 * 2.0);  // Vector2D(2.0, 4.0)
}

This is a common pattern in game development and physics-based animation code written in Dart/Flutter.

Internal Working: How Dart Evaluates Expressions

Dart compiles down to either native machine code (via AOT compilation for mobile/desktop) or JavaScript/Wasm (for web). Regardless of target, the Dart VM and compiler follow a strict left-to-right evaluation order for operands, respecting the precedence table above. What’s important to understand:

  • Numeric types: On native platforms, int is a 64-bit signed integer, and arithmetic overflows wrap around rather than throwing. On the web, because JavaScript numbers are IEEE-754 doubles, very large integers can lose precision — this is a genuine cross-platform gotcha I’ve been bitten by when doing bitwise math intended for 64-bit integers in a web-compiled Flutter app.
  • Boxing and unboxing: Dart’s num, int, and double are objects, but the VM aggressively optimizes primitive arithmetic to avoid unnecessary heap allocation in hot loops — this is part of why tight numeric loops in Dart can be surprisingly fast.
  • Short-circuit operators (&&, ||, ??) are compiled into conditional branches, not function calls, so there’s no performance penalty for using them defensively.

Performance Considerations

A few practical lessons I’ve picked up:

  • Prefer ~/ over (a / b).toInt() when you need integer division — it avoids an intermediate double allocation and rounding step.
  • Avoid excessive operator overloading on classes used in performance-critical loops (e.g., inside build() methods that run every frame); the readability gain isn’t always worth the potential indirection cost in extremely hot paths.
  • Bitwise operations on int are essentially free computationally, since they map almost directly to CPU instructions — this is why color and flag manipulation using bitwise operators is preferred over string-based encoding in Flutter’s rendering pipeline.

Common Mistakes I See (and Have Made Myself)

  1. Confusing / and ~/ — expecting integer division but getting a double, or vice versa.
  2. Using == on custom objects without overriding it, then being surprised two “equal-looking” objects don’t compare as equal.
  3. Forgetting short-circuiting doesn’t protect the right operand from side effects — if the right side has a side effect you need (like a counter increment), a short-circuited && might silently skip it.
  4. Overusing the null assertion operator (!) as a lazy fix for null safety errors, leading to runtime crashes instead of compile-time safety.
  5. Misjudging operator precedence in mixed logical/relational expressions, especially with ?? and ?: combined.

Debugging Tips for Operator-Related Bugs

  • Use print() or a debugger to inspect intermediate values rather than trusting a long chained expression.
  • When in doubt about precedence, add parentheses — it’s free and removes ambiguity for future readers (including future you).
  • For null-safety-related crashes, search your codebase for ! and audit each usage; most crashes I’ve debugged in production null-safe Dart code trace back to an overly confident null assertion.
  • Use Dart DevTools’ expression evaluator in debug mode to test operator behavior live against real object instances.

FAQs

Q: Does Dart support operator overloading for all operators? No. Dart allows overloading for a fixed set of operators (+, -, *, /, %, ~/, <, >, <=, >=, ==, [], []=, <<, >>, &, |, ^, ~, unary -), but not for things like &&, ||, or ??, since those depend on short-circuit control flow rather than simple value computation.

Q: Why does 5 / 2 give 2.5 instead of 2 in Dart? Because / is defined to always return a double for predictability. Use ~/ if you specifically want truncating integer division.

Q: Is ?? the same as a ternary operator? Not quite. ?? specifically checks for null, while the ternary condition ? a : b can branch on any boolean condition. They’re often used together, like value ?? (condition ? a : b).

Q: Can I use bitwise operators on double values? No, bitwise and shift operators in Dart only work on int. Attempting to use them on double results in a compile-time error.

Q: What’s the difference between ! (null assertion) and ! (logical NOT)? Context determines the meaning. As a prefix on a boolean expression (!isValid), it’s logical NOT. As a suffix on a nullable expression (value!), it’s the null assertion operator. They look similar but are entirely different operators.

Summary

Operators are deceptively simple on the surface but reveal a lot about a language’s philosophy once you dig in. In Dart’s case, arithmetic and relational operators behave predictably and safely (like / always returning a double), logical operators short-circuit for safe null-checking patterns, bitwise operators map cleanly to real-world use cases like color manipulation, and the null-aware and cascade operators reflect Dart’s strong opinions about safety and developer ergonomics. Understanding these details — not just the syntax, but the reasoning behind it — has made me a noticeably more confident Dart and Flutter developer, and I hope this breakdown does the same for you.

References

  • Official Dart Language Tour — Operators: https://dart.dev/language/operators
  • Official Dart Language Tour — Built-in Types: https://dart.dev/language/built-in-types
  • Dart Null Safety Documentation: https://dart.dev/null-safety
  • Flutter Color class documentation: https://api.flutter.dev/flutter/dart-ui/Color-class.html
  • Dart Language Specification: https://dart.dev/guides/language/spec
Total
1
Shares

Leave a Reply

Previous Post
Variables and Data Types in Dart

Variables and Data Types in Dart: Declaration, Type Inference, and Null Safety Explained

Next Post
Control Flow (if, else, switch) in Dart

Control Flow in Dart: If-Else, Switch-Case, and Decision-Making Statements Explained

Related Posts