Dart Programming Fundamentals

Operators (Arithmetic, Comparison, Logical, Assignment)

35 min Lesson 5 of 6

What Are Operators?

Operators are special symbols that perform operations on values (called operands). You have already seen some operators in previous lessons -- now we will cover all of Dart’s operators in one comprehensive reference. Understanding operators is essential because you will use them in every single Dart program you write.

Arithmetic Operators

Arithmetic operators perform mathematical calculations. We covered the basics earlier, but let us now look at the complete picture including increment and decrement operators.

All Arithmetic Operators

void main() {
  int a = 15;
  int b = 4;

  print(a + b);    // 19   Addition
  print(a - b);    // 11   Subtraction
  print(a * b);    // 60   Multiplication
  print(a / b);    // 3.75 Division (always double)
  print(a ~/ b);   // 3    Integer division (truncates)
  print(a % b);    // 3    Modulo (remainder)
  print(-a);       // -15  Unary negation
}

Increment and Decrement

Dart provides ++ and -- operators to increase or decrease a value by 1. The position matters -- prefix vs postfix produces different behavior when used inside an expression.

Prefix vs Postfix

void main() {
  // Prefix: changes value BEFORE using it
  int a = 5;
  int b = ++a;  // a becomes 6 first, then b gets 6
  print('a = \$a, b = \$b');  // a = 6, b = 6

  // Postfix: changes value AFTER using it
  int c = 5;
  int d = c++;  // d gets 5 first, then c becomes 6
  print('c = \$c, d = \$d');  // c = 6, d = 5

  // Same applies to decrement
  int x = 10;
  print(--x);   // 9 (prefix: decrement then print)
  print(x--);   // 9 (postfix: print then decrement)
  print(x);     // 8 (now decremented)
}
Pro Tip: When used as standalone statements (count++; or ++count;), prefix and postfix behave identically. The difference only matters when the expression is part of a larger statement like an assignment or a print call.

Assignment Operators

Assignment operators store a value in a variable. The basic assignment is =, but Dart provides compound assignment operators that combine an operation with assignment.

Basic and Compound Assignment

void main() {
  // Basic assignment
  int score = 100;

  // Compound assignment operators
  score += 10;   // score = score + 10  -> 110
  print(score);

  score -= 20;   // score = score - 20  -> 90
  print(score);

  score *= 2;    // score = score * 2   -> 180
  print(score);

  score ~/= 3;   // score = score ~/ 3  -> 60
  print(score);

  score %= 7;    // score = score % 7   -> 4
  print(score);

  // Also works with doubles
  double price = 100.0;
  price /= 3;    // price = price / 3   -> 33.333...
  print(price.toStringAsFixed(2));  // 33.33
}

Null-Aware Assignment (??=)

The ??= operator assigns a value only if the variable is currently null. If it already has a value, the assignment is skipped.

Null-Aware Assignment

void main() {
  int? a;
  print(a);      // null

  a ??= 10;      // a is null, so assign 10
  print(a);      // 10

  a ??= 20;      // a is NOT null (it is 10), so skip
  print(a);      // 10 (unchanged)

  // Practical use: setting defaults
  String? userName;
  userName ??= 'Guest';
  print(userName);  // Guest
}

Comparison (Relational) Operators

Comparison operators compare two values and return a bool result. They are the foundation of all conditional logic.

All Comparison Operators

void main() {
  int x = 10;
  int y = 20;

  print(x == y);    // false  Equal to
  print(x != y);    // true   Not equal to
  print(x > y);     // false  Greater than
  print(x < y);     // true   Less than
  print(x >= 10);   // true   Greater than or equal
  print(x <= 20);   // true   Less than or equal
}

Comparing Different Types

Type-Specific Comparison

void main() {
  // Strings compare by content
  print('Dart' == 'Dart');    // true
  print('Dart' == 'dart');    // false (case-sensitive)

  // Numbers: int and double can be compared
  print(5 == 5.0);            // true
  print(5.0 > 4);             // true

  // Booleans
  print(true == true);        // true
  print(true == false);       // false

  // Lists compare by reference, NOT by content
  var list1 = [1, 2, 3];
  var list2 = [1, 2, 3];
  print(list1 == list2);      // false (different objects!)
}
Important: In Dart, == checks value equality for primitives (int, double, String, bool) but reference equality for objects like Lists and Maps. Two lists with the same contents are NOT equal with == unless they are the exact same object.

Logical Operators

Logical operators combine boolean expressions. They are used extensively in conditions, loops, and validation logic.

Logical AND, OR, NOT

void main() {
  bool isLoggedIn = true;
  bool isAdmin = false;
  bool hasPermission = true;

  // AND (&&) -- ALL must be true
  print(isLoggedIn && isAdmin);         // false
  print(isLoggedIn && hasPermission);    // true

  // OR (||) -- at least ONE must be true
  print(isAdmin || hasPermission);       // true
  print(false || false);                 // false

  // NOT (!) -- inverts the value
  print(!isAdmin);                       // true
  print(!isLoggedIn);                    // false

  // Combining multiple operators
  bool canEdit = isLoggedIn && (isAdmin || hasPermission);
  print('Can edit: \$canEdit');  // Can edit: true
}

Short-Circuit Evaluation

Dart uses short-circuit evaluation for logical operators. This means it stops evaluating as soon as the result is determined:

Short-Circuit Behavior

bool checkFirst() {
  print('Checking first...');
  return false;
}

bool checkSecond() {
  print('Checking second...');
  return true;
}

void main() {
  // AND: if first is false, second is never checked
  print('--- AND ---');
  bool resultAnd = checkFirst() && checkSecond();
  // Output: "Checking first..." only
  // checkSecond() is never called because false && anything = false

  // OR: if first is true, second is never checked
  print('--- OR ---');
  bool resultOr = !checkFirst() || checkSecond();
  // Output: "Checking first..." only
  // checkSecond() is never called because true || anything = true
}
Note: Short-circuit evaluation is not just an optimization -- it is a useful pattern. For example, list != null && list.isNotEmpty is safe because if the list is null, the second check never runs and avoids a null error.

Bitwise Operators

Bitwise operators work on the individual bits of integer values. While not used daily, they are important for performance-critical code, flags, and low-level programming.

Bitwise Operations

void main() {
  int a = 5;   // Binary: 0101
  int b = 3;   // Binary: 0011

  print(a & b);   // 1   AND  (0001)
  print(a | b);   // 7   OR   (0111)
  print(a ^ b);   // 6   XOR  (0110)
  print(~a);      // -6  NOT  (inverts all bits)

  // Bit shifting
  print(a << 1);  // 10  Left shift  (1010) -- multiply by 2
  print(a >> 1);  // 2   Right shift (0010) -- divide by 2

  // Practical: using bit flags for permissions
  const int READ = 1;     // 001
  const int WRITE = 2;    // 010
  const int EXECUTE = 4;  // 100

  int permissions = READ | WRITE;  // 011 = 3
  print(permissions & READ);      // 1 (has read)
  print(permissions & EXECUTE);   // 0 (no execute)

  // Check if permission exists
  bool canRead = (permissions & READ) != 0;
  print('Can read: \$canRead');  // Can read: true
}

Type Test Operators

Type test operators check or cast an object’s type at runtime:

is, is!, and as

void main() {
  dynamic value = 'Hello, Dart!';

  // is -- checks if object is of a type
  if (value is String) {
    print('It is a String with length ${value.length}');
  }

  // is! -- checks if object is NOT of a type
  if (value is! int) {
    print('It is NOT an int');
  }

  // as -- type cast (throws if wrong type)
  num number = 42;
  int integer = number as int;
  print(integer);  // 42

  // Smart cast: after 'is' check, Dart auto-casts
  dynamic data = 100;
  if (data is int) {
    // Inside this block, 'data' is automatically treated as int
    print(data.isEven);  // true -- no cast needed!
  }
}

Conditional Operators

Conditional operators provide shorthand for if-else logic:

Ternary Operator (? :)

Ternary Operator

void main() {
  int age = 20;

  // condition ? valueIfTrue : valueIfFalse
  String status = age >= 18 ? 'Adult' : 'Minor';
  print(status);  // Adult

  // Can be nested (but keep it readable)
  int score = 85;
  String grade = score >= 90 ? 'A'
      : score >= 80 ? 'B'
      : score >= 70 ? 'C'
      : 'F';
  print(grade);  // B

  // Used in string interpolation
  bool isOnline = true;
  print('User is ${isOnline ? "online" : "offline"}');
}

Null-Aware Operators

Complete Null-Aware Operators

void main() {
  // ?? -- null coalescing (provide default for null)
  String? name;
  String displayName = name ?? 'Guest';
  print(displayName);  // Guest

  // ??= -- null-aware assignment
  int? count;
  count ??= 0;
  print(count);  // 0

  // ?. -- null-aware member access
  String? text;
  print(text?.length);     // null (no error!)
  print(text?.toUpperCase());  // null (no error!)

  text = 'Hello';
  print(text?.length);     // 5

  // Chaining null-aware operators
  String? city;
  int nameLength = city?.length ?? 0;
  print(nameLength);  // 0
}

Cascade Operator (..)

The cascade operator lets you make multiple operations on the same object without repeating the variable name. It is incredibly useful for configuring objects.

Cascade Operations

void main() {
  // Without cascade (repetitive)
  var buffer1 = StringBuffer();
  buffer1.write('Hello');
  buffer1.write(' ');
  buffer1.write('World');
  buffer1.write('!');
  print(buffer1);  // Hello World!

  // With cascade (clean and fluent)
  var buffer2 = StringBuffer()
    ..write('Hello')
    ..write(' ')
    ..write('World')
    ..write('!');
  print(buffer2);  // Hello World!

  // Null-aware cascade (?..)
  String? text;
  text
    ?..length  // only runs if text is not null
    ;

  // Practical: building a list
  var numbers = <int>[]
    ..add(1)
    ..add(2)
    ..add(3)
    ..addAll([4, 5, 6]);
  print(numbers);  // [1, 2, 3, 4, 5, 6]
}

Spread Operator (...)

The spread operator expands a collection into individual elements. It is commonly used when building lists or combining collections.

Spread Operator

void main() {
  var first = [1, 2, 3];
  var second = [4, 5, 6];

  // Combine lists with spread
  var combined = [...first, ...second];
  print(combined);  // [1, 2, 3, 4, 5, 6]

  // Add elements around spread
  var withExtra = [0, ...first, 99];
  print(withExtra);  // [0, 1, 2, 3, 99]

  // Null-aware spread (...?)
  List<int>? maybeNull;
  var safe = [1, 2, ...?maybeNull, 3];
  print(safe);  // [1, 2, 3]
}

Operator Precedence

When multiple operators appear in one expression, Dart follows a strict order of precedence. Here are the most important levels from highest to lowest:

Precedence Example

void main() {
  // Multiplication before addition
  print(2 + 3 * 4);        // 14 (not 20)
  print((2 + 3) * 4);      // 20 (parentheses override)

  // Comparison before logical
  print(5 > 3 && 10 < 20);   // true
  // Evaluated as: (5 > 3) && (10 < 20) = true && true

  // Assignment is lowest precedence
  int x = 2 + 3;           // x = 5 (addition happens first)

  // When in doubt, use parentheses!
  bool result = (5 + 3) > (2 * 3) && !(false || true);
  print(result);  // true && false = false
}
Pro Tip: When in doubt about operator precedence, use parentheses. They make your code clearer and prevent subtle bugs. Readable code is always better than clever one-liners.

Practice Exercise

Open DartPad and create a program that: (1) Uses all compound assignment operators (+=, -=, *=, ~/=, %=) on a variable and prints after each step. (2) Demonstrates prefix vs postfix increment in two separate print statements showing different results. (3) Uses the ternary operator to assign a letter grade based on a numeric score. (4) Uses ?? and ??= operators to handle nullable values with defaults. (5) Uses the cascade operator (..) to build a list by chaining multiple .add() calls, then prints the result.