Booleans in Dart — Singletons, Null Safety, and Logical Operators
Two values, infinite uses. A deep dive into how Dart stores true and false, what bool? actually changes, and how logical operators really work.
From bit to boolean
A transistor is essentially a switch. It can be on or off. We measure that on/off state as a bit — the smallest unit of data a computer can store.
The simplest data type built directly on top of a bit is the boolean. It can take exactly one of two values: true or false.
The name comes from George Boole, the 19th-century mathematician whose algebra of logic laid the foundation for modern computing. Every if statement, every loop, every yes/no decision our program ever makes ultimately comes down to a boolean.
In Dart, we write the type as bool — shortened from boolean.
It might seem like the simplest possible type. But like everything in Dart, there is more going on under the hood than meets the eye. Let's take a deep dive.
Declaring booleans — var, final, const
The basic form:
bool tom = false;
bool jerry = true;Three parts: the type (bool), the name (tom), and the value (false). Pretty standard.Writing
bool every time is repetitive. Dart can figure out the type from the value on the right, so we can use var instead:var tom = false; // Dart infers this is a bool
var jerry = true; // also a boolBoth forms produce identical bytecode. Most idiomatic Dart leans on var for local variables and spells out the type only when it adds clarity.When we want to lock down a value so it cannot be reassigned, Dart gives us two keywords:
•
final — set once, at runtime. The value can be computed when the program runs.•
const — set once, at compile time. The value must be known before the program even starts.final bool isHuman = true;
const bool piIsThree = false;For booleans, const fits naturally because true and false are themselves compile-time constants. A rule of thumb: reach for const if we can, final if we can't, and plain var only when the value genuinely needs to change. Canonical instances — true and false are singletons
Here is a neat optimization. We might assume that every time we write false in our code, Dart creates a fresh boolean object in memory. But that would be wasteful. There are only two possible boolean values in the entire universe, and our program might reference them thousands of times.
Instead, the Dart VM creates exactly one true object and one false object, and reuses them everywhere. These are called canonical instances. Some people just call them singletons.
So when we write bool tom = false;, Dart does not build a new boolean object. The variable tom simply holds a reference — a tiny pointer — to the one shared false object that already exists.
We can prove this in DartPad:
void main() {
bool tom = false;
bool jerry = false;
print(identical(tom, jerry)); // true
}identical() checks whether two variables point to the exact same object in memory. The fact that it returns true is direct proof of the canonical-instance trick.The takeaway: declaring a boolean variable in Dart is essentially free. We are not allocating new objects — we are just creating a label that points to an object that already exists.
Null safety — what bool? adds
In Dart, a plain bool can hold exactly two values: true and false. It cannot be null.
But what if we genuinely need a third state — something like "we don't know yet"? Dart lets us opt into nullability by adding a ? to the type:
bool? hasAnswered; // can be true, false, OR null
hasAnswered = null; // perfectly legal
hasAnswered = true; // also legalThis is one of Dart's most important features: no variable can be null unless we explicitly say so. With bool?, there are now exactly three values a bool-shaped variable can point to.In practice: use
bool whenever we can. Reach for bool? only when "not yet known" is a meaningful state — typically for unresolved user inputs, optional API responses, or three-state UI toggles. bool? also costs slightly more, because a slot that must also be able to hold null can't take advantage of the same simple representation as plain bool. Where booleans come from
So far we have been writing true and false by hand. In real code, we rarely do that. Booleans almost always come from comparison operators.
int age = 18;
bool canVote = age >= 18; // true
bool isMinor = age < 18; // false
bool isExactlyEighteen = age == 18; // trueThe comparison operators in Dart:•
== — equal to•
!= — not equal to•
< — less than•
> — greater than•
<= — less than or equal to•
>= — greater than or equal toEvery one of these expressions evaluates to a
bool. That is the bridge between the rest of our program — numbers, strings, objects — and the boolean world.When we test if a list is empty, when we check if two strings match, when we ask if a value is above some threshold — all of those produce booleans, which then feed into
if statements, while loops, and ternary expressions. Logical operators and short-circuit evaluation
Now that we have bools, what can we do with them? Let's explore the three core logical operators.
void main() {
bool tom = false;
bool jerry = true;
print(!tom); // true — the NOT operator flips the value
print(tom && jerry); // false — AND requires both to be true
print(tom || jerry); // true — OR requires at least one to be true
}Short-circuit evaluation.
&& and || are smart — they stop evaluating as soon as the answer is certain. For A && B, if A is false, Dart does not bother checking B. For A || B, if A is true, Dart skips B.This is more useful than it sounds:
if (user != null && user.isActive) {
// safe to access user.isActive
// if user is null, the && stops short
}Without short-circuiting, this would crash whenever user is null. With it, the right side is only evaluated when the left side guarantees it is safe to do so. Bitwise gotcha, XOR, ternary, and precedence
Three more operators to know, and the rules about which goes first.
The bitwise gotcha: & and |
Dart also has & and | (single-character versions). They work on booleans, but with a critical difference: they do not short-circuit. Both sides are always evaluated.
bool a = false & expensiveCheck(); // expensiveCheck() IS called
bool b = false && expensiveCheck(); // expensiveCheck() is NOT calledIn almost every case, we want && and ||, not & and |. Treat the single-character versions as a footgun unless we have a specific reason.XOR — exclusive OR
There is also
^, which returns true if exactly one of its operands is true:print(true ^ false); // true
print(true ^ true); // false
print(false ^ false); // falseRare in everyday code, but handy for toggles and parity checks.The ternary operator
Sometimes we want to choose between two values based on a condition. The ternary gives us a one-liner:
// condition ? value if true : value if false
String status = jerry ? "Jerry is awake!" : "Jerry is sleeping...";
print(status); // Jerry is awake!We read it as "if jerry is true, use the first value; otherwise, use the second."Who goes first?
When we combine operators without parentheses, Dart evaluates them in a strict order. From highest to lowest, the ones relevant to booleans:
1.
! (NOT)2.
<, >, <=, >= (relational)3.
==, != (equality)4.
& (bitwise AND)5.
^ (XOR)6.
| (bitwise OR)7.
&& (logical AND)8.
|| (logical OR)9.
? : (ternary)When in doubt, use parentheses. They override everything and make our code much easier to read.
Strict truthiness and utility methods
One last thing, and this trips up newcomers from JavaScript, Python, or C constantly.
Dart requires real booleans. In Dart, the condition of an if (or a while, or any boolean context) must be a real bool. Dart will not auto-convert numbers, strings, or other objects.
if (1) { ... } // compile error
if ("hello") { ... } // compile error
if (someList) { ... } // compile error
if (someList.isNotEmpty) { ... } // ok — that's a real bool
if (count > 0) { ... } // okThis strictness eliminates a whole class of subtle bugs. If we are coming from somewhere else, the rule is simple. Be explicit. Compare what we mean.Two utility methods worth knowing.
// Parse a bool from a string (useful for CLI args, JSON, etc.)
bool b = bool.parse("true"); // true
bool? b2 = bool.tryParse("nope"); // null (didn't parse)
// Read a compile-time flag passed via --define
const bool inDebugMode = bool.fromEnvironment("debug", defaultValue: false);We won't need these on day one, but it is good to know they exist.That's the full tour of
bool in Dart: what it is, how Dart stores it, where it comes from, what we can do with it, and the small print that catches newcomers. Master this and we have a solid foundation for everything that follows — because every if, every while, every conditional decision in our program ultimately comes down to a single boolean. Test your understanding
7 questions
Seven questions covering singletons, null safety, comparisons, short-circuit, and Dart's strict truthiness.