Dart 6 🎯 Operators: Arithmetic, Relational, Type Test (is, as), and Bitwise
Operators are the symbols that perform computation on values. Dart provides the familiar arithmetic operators, comparison and equality operators, logical operators, and assignment operators that every C-family language has, plus a few that are distinctive: the type test and type cast operators (is and as), the null-aware operators (??, ?., ??=), and the cascade notation (..) that lets you call multiple methods on the same object. The type operators matter because Dart is statically typed and soundly null-safe; the null-aware operators matter because Dart has made null safety a core part of the language since version 2.12.
Understanding Dart’s operators is not just about memorizing symbols. It is about understanding the type system’s interaction with runtime values: when is performs a type check, when as performs a cast, when a null-aware operator short-circuits, and how the cascade notation avoids the temporary-variable pattern that plagues method chaining in other languages. These are the features that make Dart code concise and safe at the same time.
This chapter covers arithmetic operators, relational and equality operators, logical operators, type test and cast operators, bitwise operators, null-aware operators, assignment operators, and the cascade notation.
Key point: Dart’s operators include the standard arithmetic, relational, logical, and bitwise sets, plus type operators (is, as) for runtime type checks and casts, null-aware operators (??, ?., ??=) for safe null handling, and the cascade notation (..) for chaining calls on the same object.
Why these operators exist
The computation problem. Arithmetic and relational operators are the foundation of any expression. Dart follows the C-family conventions with +, -, *, /, %, ==, !=, <, >, <=, >=, so developers coming from other languages recognize them immediately.
The type safety problem. Dart is statically typed with sound null safety. The is operator lets you check a value’s runtime type before using it, and as performs a cast when the developer knows the type more precisely than the compiler. Without these, working with dynamic values, JSON, or polymorphic collections would require verbose workarounds.
The null problem. Null safety means a variable of type String? might be null and must be checked before use. The null-aware operators — ??, ?., ??= — provide concise ways to handle null without writing explicit if (x != null) blocks. This is a deliberate design choice that makes null handling part of the expression syntax rather than a separate statement.
The chaining problem. In many languages, configuring an object involves a temporary variable: var b = Builder(); b.setA(1); b.setB(2); return b.build();. The cascade notation in Dart writes the same thing without the temporary: Builder()..setA(1)..setB(2)..build(). This is used heavily in Flutter for widget configuration.
The bit manipulation problem. Systems programming and low-level data handling require bitwise operations. Dart provides the full set: &, |, ^, ~, <<, >>, >>>. The unsigned shift operator >>> is notable because Dart integers are signed by default, and >>> shifts without sign extension.
a. Arithmetic operators
Dart’s arithmetic operators follow standard conventions.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 5 + 3 | 8 |
- | Subtraction | 5 - 3 | 2 |
* | Multiplication | 5 * 3 | 15 |
/ | Division | 10 / 3 | 3.3333... |
~/ | Integer division | 10 ~/ 3 | 3 |
% | Modulo | 10 % 3 | 1 |
-expr | Unary negation | -x | Negated value |
++ | Increment | x++, ++x | Increments by 1 |
-- | Decrement | x--, --x | Decrements by 1 |
The division operator / always produces a double, even when both operands are integers:
print(10 / 3); // 3.3333333333333335
print(10 ~/ 3); // 3
The integer division operator ~/ produces an int by truncating toward zero. Modulo % returns the remainder and works with both integers and doubles.
print(10 % 3); // 1
print(10.5 % 3); // 1.5
The increment and decrement operators have prefix and postfix forms. Prefix increments before returning the value; postfix returns the value then increments.
var a = 5;
print(a++); // 5 (returns a, then increments)
print(a); // 6
var b = 5;
print(++b); // 6 (increments, then returns)
b. Relational and equality operators
| Operator | Meaning |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
The equality operator == in Dart is a method on Object, which means classes can override it to define value equality. For most built-in types, == compares values. For custom classes, the default implementation compares identity unless == is overridden.
print(5 == 5); // true
print(5 != 3); // true
print('abc' == 'abc'); // true
print([1, 2] == [1, 2]); // false — different list instances
The list comparison returns false because List does not override == to compare elements. This is a common source of confusion. To compare lists by value, use listEquals from package:collection or compare element by element.
Dart’s == works correctly with null: null == null is true, and null == 5 is false.
c. Logical operators
| Operator | Meaning | Example |
|---|---|---|
&& | Logical AND | a && b |
|| | Logical OR | a || b |
! | Logical NOT | !a |
Logical operators work with bool values. && and || are short-circuit operators: the right operand is evaluated only if needed.
bool isAdult = age >= 18;
bool canVote = isAdult && isCitizen;
bool isWeekend = day == 'Sat' || day == 'Sun';
bool isInvalid = !isValid;
Short-circuit behavior:
// If x is null, the right side is not evaluated
if (x != null && x.length > 0) { ... }
// If the first condition is true, the second is not evaluated
if (isCached || fetchFromNetwork()) { ... }
d. Type test and cast operators
Dart provides two operators for working with types at runtime.
| Operator | Name | Purpose |
|---|---|---|
is | Type test | Returns true if the object is of the type |
is! | Negated type test | Returns true if the object is not of the type |
as | Type cast | Casts the object to the type, throws on failure |
The is operator performs a type check:
var value = 'hello';
if (value is String) {
print(value.toUpperCase());
}
if (value is! int) {
print('Not an integer');
}
Dart’s type promotion means that after an is check, the variable is treated as the checked type within the block:
Object obj = 'hello';
if (obj is String) {
print(obj.length); // obj is promoted to String
}
The as operator performs an explicit cast:
var value = json['name'] as String;
If the cast fails at runtime, Dart throws a TypeError. The as operator is used when the developer knows the type more precisely than the compiler can infer, such as when parsing JSON.
A safer alternative to as is to check with is first:
if (value is String) {
print(value.length);
} else {
print('Not a string');
}
e. Bitwise operators
Bitwise operators work on the binary representation of integers.
| Operator | Name | Example | Result |
|---|---|---|---|
& | AND | 0b1100 & 0b1010 | 0b1000 |
| | OR | 0b1100 | 0b1010 | 0b1110 |
^ | XOR | 0b1100 ^ 0b1010 | 0b0110 |
~ | NOT | ~0b1100 | -0b1101 |
<< | Left shift | 1 << 3 | 8 |
>> | Right shift | 16 >> 2 | 4 |
>>> | Unsigned right shift | -1 >>> 28 | 15 |
The ~ operator flips all bits, producing a negative number in two’s complement representation. The << operator shifts bits left, multiplying by powers of two. The >> operator shifts right with sign extension, and >>> shifts right without sign extension.
print(0b1100 & 0b1010); // 8
print(0b1100 | 0b1010); // 14
print(1 << 4); // 16
print(-8 >> 1); // -4
print(-8 >>> 1); // 9223372036854775804
Bitwise operators are commonly used for flags, permissions, and low-level data manipulation. The & operator combined with a mask isolates specific bits; | sets bits; ^ toggles bits.
f. Null-aware operators
Dart’s null safety means a nullable type (String?) may be null. The null-aware operators provide concise handling.
| Operator | Name | Meaning |
|---|---|---|
?? | If-null | Returns left if not null, otherwise right |
??= | Null-aware assignment | Assigns only if the variable is null |
?. | Null-aware access | Calls or accesses only if not null |
The ?? operator provides a default:
String? name;
print(name ?? 'Guest'); // Guest
name = 'Alice';
print(name ?? 'Guest'); // Alice
The ??= operator assigns only when the variable is null:
String? name;
name ??= 'Guest'; // assigns
name ??= 'Bob'; // does not assign; name is already 'Guest'
print(name); // Guest
The ?. operator short-circuits method calls and property access:
String? name;
print(name?.length); // null, no error
name = 'Alice';
print(name?.length); // 5
Null-aware access can be chained:
user?.address?.city?.toUpperCase();
Each ?. short-circuits if the preceding value is null, so no step in the chain throws.
g. Assignment operators and cascade notation
Dart provides compound assignment operators for combining an operation with assignment.
| Operator | Equivalent |
|---|---|
+= | x = x + y |
-= | x = x - y |
*= | x = x * y |
/= | x = x / y |
~/= | x = x ~/ y |
%= | x = x % y |
&= | x = x & y |
|= | x = x | y |
^= | x = x ^ y |
<<= | x = x << y |
>>= | x = x >> y |
>>>= | x = x >>> y |
??= | x = x ?? y |
The cascade notation .. calls multiple methods on the same object and returns the object itself:
var list = [3, 1, 2]
..sort()
..add(4)
..remove(1);
print(list); // [2, 3, 4]
Without cascade, this would require a temporary variable and separate statements:
var list = [3, 1, 2];
list.sort();
list.add(4);
list.remove(1);
The cascade is especially common in Flutter for building widget trees:
var paint = Paint()
..color = Colors.blue
..strokeWidth = 5.0
..style = PaintingStyle.stroke;
The cascade returns the Paint object, so it can be assigned or passed in the same expression.
A cascade can also use ?.. for null-aware cascading:
user?..name = 'Alice'
..age = 30;
The entire cascade is skipped if user is null.
Complete Example Session
// ============================================
// PART 1: ARITHMETIC OPERATORS
// ============================================
void main() {
print(10 + 3); // 13
print(10 - 3); // 7
print(10 * 3); // 30
print(10 / 3); // 3.333...
print(10 ~/ 3); // 3
print(10 % 3); // 1
}
// ============================================
// PART 2: INCREMENT AND DECREMENT
// ============================================
void main() {
var a = 5;
print(a++); // 5
print(a); // 6
var b = 5;
print(++b); // 6
print(b); // 6
}
// ============================================
// PART 3: RELATIONAL AND EQUALITY
// ============================================
void main() {
print(5 == 5); // true
print(5 != 3); // true
print(5 > 3); // true
print(5 <= 5); // true
print(null == null); // true
}
// ============================================
// PART 4: LOGICAL OPERATORS
// ============================================
void main() {
var age = 25;
var isCitizen = true;
var canVote = age >= 18 && isCitizen;
print(canVote); // true
var isWeekend = false;
var isHoliday = true;
print(isWeekend || isHoliday); // true
}
// ============================================
// PART 5: TYPE TEST WITH is
// ============================================
void main() {
Object value = 'hello';
if (value is String) {
print(value.length); // 5 (value promoted to String)
}
if (value is! int) {
print('Not an integer');
}
}
// ============================================
// PART 6: TYPE CAST WITH as
// ============================================
void main() {
Map<String, dynamic> json = {'name': 'Alice', 'age': 30};
var name = json['name'] as String;
var age = json['age'] as int;
print('$name is $age years old');
}
// ============================================
// PART 7: BITWISE OPERATORS
// ============================================
void main() {
const READ = 0x1; // 0b0001
const WRITE = 0x2; // 0b0010
const EXECUTE = 0x4; // 0b0100
var permissions = READ | WRITE;
print(permissions & READ); // 1 (has read)
print(permissions & EXECUTE); // 0 (no execute)
permissions |= EXECUTE;
print(permissions); // 7
}
// ============================================
// PART 8: NULL-AWARE OPERATORS
// ============================================
void main() {
String? name;
print(name ?? 'Guest'); // Guest
name ??= 'Alice';
print(name); // Alice
print(name?.length); // 5
name = null;
print(name?.length); // null
}
// ============================================
// PART 9: COMPOUND ASSIGNMENT
// ============================================
void main() {
var x = 10;
x += 5; // 15
x -= 3; // 12
x *= 2; // 24
x ~/= 5; // 4
x <<= 2; // 16
print(x); // 16
}
// ============================================
// PART 10: CASCADE NOTATION
// ============================================
void main() {
var list = [3, 1, 2]
..sort()
..add(4)
..remove(1);
print(list); // [2, 3, 4]
// Null-aware cascade
List<int>? maybeList;
maybeList?..add(1)..add(2);
print(maybeList); // null (entire cascade skipped)
}
These ten parts cover the full range of Dart operators: arithmetic, increment/decrement, relational and equality, logical, type test and cast, bitwise, null-aware, compound assignment, and cascade notation. Each pattern is idiomatic Dart.
Quick Reference
Arithmetic Operators
| Operator | Meaning | Example |
|---|---|---|
+ | Addition | 5 + 3 |
- | Subtraction | 5 - 3 |
* | Multiplication | 5 * 3 |
/ | Division (double) | 10 / 3 |
~/ | Integer division | 10 ~/ 3 |
% | Modulo | 10 % 3 |
Relational and Logical
| Operator | Meaning |
|---|---|
==, != | Equality |
<, >, <=, >= | Comparison |
&&, ||, ! | Logical AND, OR, NOT |
Type Operators
| Operator | Purpose |
|---|---|
is | Type test (with promotion) |
is! | Negated type test |
as | Type cast (throws on failure) |
Bitwise Operators
| Operator | Meaning |
|---|---|
& | AND |
| | OR |
^ | XOR |
~ | NOT |
<< | Left shift |
>> | Signed right shift |
>>> | Unsigned right shift |
Null-Aware Operators
| Operator | Meaning |
|---|---|
?? | Return left if not null, else right |
??= | Assign if null |
?. | Access if not null |
?.. | Cascade if not null |
Cascade Notation
| Syntax | Example |
|---|---|
.. | Paint()..color = red..strokeWidth = 2 |
?.. | user?..name = 'Alice'..age = 30 |
Best Practices
✅ Do This:
// Use is for type checks
if (value is String) print(value.length);
// Use ?? for defaults
var name = input ?? 'Guest';
// Use ?. for nullable access
print(user?.name?.length);
// Use cascade for configuration
var paint = Paint()
..color = Colors.blue
..strokeWidth = 2;
// Use bitwise for flags
var flags = READ | WRITE;
❌ Don’t Do This:
// Avoid as without a preceding check
var s = value as String; // ❌ may throw
// Avoid unsafe null access
print(user!.name); // ❌ crashes if user is null
// Avoid temp variables for configuration
var p = Paint(); // ❌ use cascade instead
p.color = Colors.blue;
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
/ returns double | Division always produces double | Use ~/ for integer result |
| List equality fails | == compares identity for lists | Use listEquals from collection |
as throws TypeError | Value is not the expected type | Check with is before casting |
| Null check throws | Using ! on a null value | Use ?. or ?? instead |
| Cascade reassigns | Using = instead of .. | Use .. for cascades |
?? vs ?: | Dart has no ternary shorthand with null | Use ?? for null, ?: for condition |
Real-World Examples
1. Safe Division
int average(List<int> nums) {
if (nums.isEmpty) return 0;
return nums.reduce((a, b) => a + b) ~/ nums.length;
}
2. Permission Flags
const READ = 1, WRITE = 2, EXECUTE = 4;
var perms = READ | WRITE;
bool canWrite = (perms & WRITE) != 0;
3. JSON Parsing with as
Map<String, dynamic> json = jsonDecode(response);
var name = json['name'] as String;
var age = json['age'] as int;
4. Type Check with Promotion
void printLength(Object value) {
if (value is String) {
print(value.length); // promoted
} else if (value is List) {
print(value.length); // promoted
}
}
5. Default Value with ??
String getGreeting(String? name) {
return 'Hello, ${name ?? 'Guest'}!';
}
6. Null-Aware Chain
var city = user?.address?.city ?? 'Unknown';
7. Null-Aware Assignment
var cache = <String, String>{};
String? cached;
cached ??= fetchFromNetwork();
8. Cascade Configuration
var controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..addListener(() => setState(() {}))
..forward();
9. Null-Aware Cascade
StringBuffer? buffer;
buffer?..write('Hello')
..write(' ')
..write('World');
10. Bit Manipulation
int toggleBit(int value, int position) {
return value ^ (1 << position);
}
Visual
Operator Categories
┌──────────────────────────────────────────────────────────────┐
│ DART OPERATOR CATEGORIES │
│ │
│ ARITHMETIC │
│ + - * / ~/ % ++ -- -expr │
│ │
│ RELATIONAL / EQUALITY │
│ == != < > <= >= │
│ │
│ LOGICAL │
│ && || ! │
│ │
│ TYPE │
│ is is! as │
│ │
│ BITWISE │
│ & | ^ ~ << >> >>> │
│ │
│ NULL-AWARE │
│ ?? ??= ?. ?.. │
│ │
│ ASSIGNMENT │
│ = += -= *= /= ~/= %= &= |= ^= <<= >>= >>>= ??= │
│ │
│ CASCADE │
│ .. ?.. │
└──────────────────────────────────────────────────────────────┘
Type Test and Promotion
┌──────────────────────────────────────────────────────────────┐
│ is PROMOTES THE TYPE WITHIN THE BLOCK │
│ │
│ Object value = 'hello'; │
│ │
│ if (value is String) { │
│ // value is promoted to String here │
│ print(value.length); │
│ } │
│ │
│ Without promotion: │
│ if (value is String) { │
│ print((value as String).length); // explicit cast │
│ } │
│ │
│ Promotion is why is is preferred over as when possible. │
└──────────────────────────────────────────────────────────────┘
Null-Aware Operators
┌──────────────────────────────────────────────────────────────┐
│ ?? a ?? b → a if a is not null, else b │
│ ??= a ??= b → assign b to a if a is null │
│ ?. a?.b → access b if a is not null │
│ ?.. a?..b() → cascade if a is not null │
│ │
│ String? name; │
│ print(name ?? 'Guest'); // Guest │
│ name ??= 'Alice'; // name = 'Alice' │
│ print(name?.length); // 5 │
│ print(name?.toUpperCase()); // ALICE │
└──────────────────────────────────────────────────────────────┘
Cascade Notation
┌──────────────────────────────────────────────────────────────┐
│ WITHOUT CASCADE WITH CASCADE │
│ │
│ var paint = Paint(); var paint = Paint() │
│ paint.color = blue; ..color = blue │
│ paint.strokeWidth = 2; ..strokeWidth = 2 │
│ paint.style = stroke; ..style = stroke; │
│ │
│ The cascade returns the object itself, allowing │
│ assignment or passing in the same expression. │
│ Commonly used for widget configuration in Flutter. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Arithmetic | +, -, *, /, ~/, % |
| Integer division | ~/ returns int |
| Division result | / always returns double |
| Relational | ==, !=, <, >, <=, >= |
| Logical | &&, ||, ! (short-circuit) |
| Type test | is, is! (with type promotion) |
| Type cast | as (throws on failure) |
| Bitwise | &, |, ^, ~, <<, >>, >>> |
| Null-aware | ??, ??=, ?., ?.. |
| Cascade | .., ?.. |
| Compound assignment | +=, -=, *=, /=, ~/=, %=, etc. |
Key takeaways:
- Dart’s arithmetic operators follow C conventions, with
~/for integer division. The/operator always produces adouble;~/truncates to anint. - The
isoperator performs a type test and promotes the variable. Afterif (value is String), the variable is treated asStringwithin the block, so no explicit cast is needed. - The
asoperator performs a cast that throws on failure. Use it when the type is known more precisely than the compiler can infer, such as when parsing JSON. Preferiswhen a check is acceptable. - Null-aware operators handle nullable values concisely.
??provides a default,??=assigns if null, and?.accesses only if not null. - The cascade notation calls multiple methods on the same object.
..returns the object itself, eliminating the temporary variable pattern. It is heavily used in Flutter. - Bitwise operators work on integers.
&,|,^,~,<<,>>, and>>>are available. The unsigned right shift>>>shifts without sign extension. - Compound assignment operators combine an operation with assignment.
x += 5is equivalent tox = x + 5, and the same pattern applies to all binary operators.
Remember: Dart’s operators are the symbols that make expressions possible, and the language’s distinctive additions — type test, null-aware, and cascade — reflect its priorities. Static typing with sound null safety means the compiler needs a way to express runtime type checks (is), casts (as), and null handling (??, ?.) within expressions rather than as separate statements. The cascade notation reflects the Flutter idiom of configuring objects in a single expression. Understanding these operators is understanding how Dart code reads: type checks that promote, null handling that short-circuits, and configuration that chains without temporary variables. The standard arithmetic, relational, logical, and bitwise operators are familiar; the null-aware and type operators are where Dart’s character shows.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!