Flutter projects can use both platform-specific and cross-platform code. The latter is written in Dart, and, for building Flutter apps, some basic knowledge of Dart is required.
Fluttering Dart's goal is to explore fundamental knowledge and unveil tips & tricks of the powerful programming language that brings Flutter to life.
In the first two parts of the series, we went through the Dart built-in data types and functions.
In this part, we'll discover Dart's operators: how to use and abuse or even override their behavior.
Some of the code examples can be tried out, and played with, using DartPad.
Operators
An operator is any symbol that we use in our code that allows us to do mathematical calculations, perform boolean logic or do something else like string concatenation or calculate intersecting sets.
Operators behave like functions, though their syntax or semantic is different from what we'd expect from a function.
Based on their position there are prefix, infix or postfix operators and based on their number of operands they are classified as unary — one operand, binary — two operands or ternary — three operands.
For operators that work on two operands, the leftmost operand's version of the operator is used. For example, if we have a String object and an int object, sVal + iVal uses the String version of +, and will require to use int's toString() method to cast the integer value to String.
// unary prefix operator
++one
// infix binary operator
one + two
// unary postfix
one++
// ternary operator
one ? two : threeThe semantics of operators vary with the evaluation strategy and argument passing mode. An expression that contains an operator is evaluated in some way, and the result might be just a value or might be an object allowing assignment. As with Dart, we already found out that all results are objects.
// example of operator
a + b
// and equivalent function
add(a, b)Precedence
As we can see bellow, Dart has many operators. Their precedence decides what is the priority of each operator while executing our beautiful code.
Associativity applies to operators with the same precedence. Most of the operators have left-to-right associativity (the direction being pointed by a little arrow). The assignment and conditional operators have it right-to-left and the operators with no arrow in front have no associativity.
To exemplify, I'll use the precedence of multiplication on addition (that you might remember from math classes):
r = a + b * c;The above expression, when executed, will detect all operators and will execute them in the order of precedence. If we look at the list above, we'll identify 3 operators (in the order of appearance):
=is the first and least "important"+is the second and a bit more "important" compared to=*is the last and the most "important" of all
So we have, in order of importance, *, + and = (ironically, the exact opposite of their appearance).
First, b will be multiplied * with c. Then the result will be added + to c and, finally, the result will be assigned = to r.
In order to read this more easily we usually write expressions the way they are evaluated:
// easier to read
r = a + (b * c);Categories
+Addassert(5 + 3 == 8);-Subtractassert(5 - 3 == 2);-exprUnary minus — reverses the sign of the expressionassert(-(5 + 3) == -8);*Multiplyassert(5 * 3 == 15);/Divideassert(5 / 3 == 1.6666666666666667);~/Divide returning the integer resultassert(5 / 3 == 1);%Modulo — returns the remainder of an integer divisionassert(5 % 3 == 2);expr++Postfix increment — expression value isexpr++exprPrefix increment — expression value isexpr+ 1expr--Postfix increment — expression value isexpr--exprPrefix increment — expression value isexpr- 1
Prefix and postfix increment and decrement operators are a bit tricky to understand:
var a, b;
a = 0;
b = ++a; // increments a first, then assigns the value to b
assert(b == a); // 1 == 1
a = b++; // first assigns the value to a, then increments a
assert(b != a); // 2 != 1
print('a=$a and b=$b'); // prints a=1 and b2Postfix operation expr++ or expr-- is a bit more inefficient than the prefix one ++expr or ++expr. In situations where the temporary value that is created by the postfix is not used the prefix should be used.
This breaks the precedence rules from before. What can I tell? Rules are meant to be broken!
Dart Programming Language Specification (version 2.2) explanation:
Evaluation of a postfix expression e of the form v++ respectively v--, where v is an identifier, proceeds as follows: Evaluate v to an object r and let y be a fresh variable bound to r. Evaluate v = y + 1 respectively v = y -- 1. Then e evaluates to r.
The above ensures that if the evaluation involves a getter, it gets called exactly once. Likewise in the cases below.
==Equalassert(3 == 3);!=Not equalassert(5 != 3);>Greater thanassert(5 > 3);<Less thanassert(5 < 8);>=Greater than or equal toassert(5 >= 5);<=Less than or equal toassert(5 <= 5);
Tests if the objects represent the same thing. For verifying if 2 objects are exactly the same we should use the identical() function.
The operation is invoked on the first operand.
asType cast operatorisType test — true if the object has the specified typeis!Type test — false if the object has the specified type
The last two operators basically test if two objects are the same type (equivalent to == and !=).
// because this operators work with objects, we'll use the fictional Cat and Dog these classes extend the Pet class
List pets = <Pet>[Cat('Simba'), Cat('Micuta')];
Cat oneCat = (pets[0] as Cat);
Cat anotherCat = (pets[1] as Cat);
assert(oneCat.name == 'Simba');
assert(anotherCat.name == 'Micuta');
assert(anotherCat is Cat);
assert(oneCat is! Dog);Values can be assigned using the = operator. To assign only if the assigned-to variable is null, use the ??= null-aware operator.
Every operator from above except for the actual = operator is a compound operator.
That means that operation is combined with an assignment. The actual operations descriptions can be found in their respective categories.
!exprInverts the expression (toggles between false and true)assert(!true == false);&&Logical ANDassert(true && false == false);||Logical ORassert(true || false == true);
We can use these to invert or combine boolean expressions.
&AND takes two equal-length binary representations and performs the logical AND operation on each pair of the corresponding bits, which is equivalent to multiplying them. If both bits in the compared position are 1, the bit in the resulting binary representation is 1 (assert(1 & 1 == 1);) otherwise, the result is 0 (assert(0 & 1 == 0);,assert(1 & 0 == 0), andassert(0 & 0 == 0);)|OR takes two equal-length binary representations and performs the logical inclusive OR operation on each pair of corresponding bits. The result in each position is 0 if both bits are 0 (assert(0 | 0 == 0);), while otherwise the result is 1 (assert(0 | 1 == 1);,assert(1 | 0 == 1), andassert(1 | 1 == 1);)^XOR takes two equal-length binary representations and performs the logical exclusive OR operation on each pair of corresponding bits. The result in each position is 1 if only one of the bits is 1 (assert(0 ^ 1 == 1);andassert(1 ^ 0 == 1);), but will be 0 if both are 0 or both are 1 (assert(1 ^ 1 == 0);andassert(0 ^ 0 == 0);)~exprUnary bitwise complement toggles all bits between 0 and 1. (assert(1 & ~1 == 0);)<<Shift left shifts the bits to the left and zeros are shifted in as new digit. (assert(1 << 1 == 2);)>>Shift right shifts the digits to the right and zeros are shifted in to replace the discarded bits. (assert(1 >> 1 == 0);)
In Dart, we can use bitwise and shift operators for manipulating bits of binary data.
4294967295 is the number that we'll get if we complement 0. That is (2 raised to the power 32)–1, or the max value of unsigned int 32 bit.
assert(~0 == math.pow(2,32) — 1);The actual binary representation of 4294967295 (in decimal base) is:
11111111111111111111111111111111I'm planning to go in detail with the bitwise and shift operations in real life use cases. If interested keep an eye on me.
expr ? expr1 : expr2Ifexpris true, evaluatesexpr1(and returns its value); otherwise, evaluates and returns the value ofexpr2expr1 ?? expr2Ifexpr1is non-null, returns its value; otherwise, evaluates and returns the value ofexpr2. This null-aware operator is useful, for example, when you want to make sure you default to a value in case the first expression is null:
class Cat {
String name;
}
void main() {
// we create a generic cat
Cat genericCat = Cat();
// we create a stray cat
Cat strayCat = Cat();
// we set the stray cat's name
// to the generic cat's name value
// that will be null so our stray cat
// name will be Catonymous
strayCat = genericCat.name ?? 'Catonymous';
}Cascades (..) allow us to make a sequence of operations on the same object. In addition to function calls, we can also access fields on that same object. This often saves us from creating a temporary variable and allows us to write more fluid code.
// we'll use the fictional Cat class we used before
Cat()
..name = 'Simba'
..feed()
..pet();The example above reads quite fluently in plain English (the beauty of cascades): we get a stray cat out of nowhere, name it Simba and then feed and pet it so it can happily meow and purr.
()Function application — represents a function call[]List access — refers to the value at the specified index in the list.Member access — refers to a property of an expression?.Conditional member access —a null-aware operator that acts like the above, but the leftmost operand can be null
We've used these all over the place in the previous examples (?. makes an exception, and also might help us avoid catching one).
The conditional member access, ?., is useful when we want (or need) to get away with using objects that can be null without throwing an exception.
class Cat {
String name;
}
void main() {
Cat strayCat;
// prints null
print(t?.name);
// throws null is not an object exception
print(t.name);
}Overriding
Have you ever dreamed of defining your custom behavior for certain operators? Dart allows you to fulfill your dreams via overriding.
Be aware that only a subset of Dart's operators can be overridden. These are [], []=, ~, *, /, ~/, %, +, -, <<, >>, &, ^, |, <, >, <=, >= and ==.
!= is not an overridable operator. The expression expr1 != expr2 is just a shortcut to !(expr1 == expr2), so if we override == we override != as well.
For some classes, using operators is more concise than using methods. For example, the List class overrides the + operator for list concatenation. The code [a, b, c] + [d, e, f] is very easy to understand.
We finally define a Cat class and want to compare our cats' weights. We do that by overriding the relational and equality operators.
class Cat {
final String name;
final double weight;
Cat({this.name, this.weight});
bool operator ==(Object cat) => this.hashCode == cat.hashCode;
String operator <(Cat cat) => (weight < cat.weight)?'$name is lighter than ${cat.name}.':'$name is heavier than ${cat.name}.';
String operator >(Cat cat) => (weight > cat.weight)?'$name is heavier than ${cat.name}.':'$name is lighter than ${cat.name}.';
String operator >=(Cat cat) => (weight >= cat.weight)?((this==cat)?'$name is as heavy as ${cat.name}.':'$name is heavier than ${cat.name}.'):'$name is lighter than ${cat.name}.';
String operator <=(Cat cat) => (weight <= cat.weight)?((this==cat)?'$name is as light as ${cat.name}.':'$name is lighter than ${cat.name}.'):'$name is heavier than ${cat.name}.';
@override
int get hashCode => weight.hashCode;
}
void main() {
// create our cats
Cat oneCat = Cat(name: 'Simba', weight: 4.1);
Cat anotherCat = Cat(name: 'Micuta', weight: 3.8);
Cat yetAnotherCat = Cat(name: 'Saki', weight: 3.8);
Cat meowCat = Cat(name: 'Klein', weight: 5.0);
// and compare them
print(oneCat>anotherCat);
print(yetAnotherCat<=anotherCat);
print(meowCat<=oneCat);
}These overriding examples are a bit more complex because they change even the expression's evaluation result type from bool to String, and >= and <= even make use of two mixed conditional operators. We've done this to overcome the fact that we can't change the returning type of ==, which must remain boolean.
Note that we've overridden the hashCode getter (it is a must when overriding the == operator).
The result of the above code is:
Simba is heavier than Micuta.
Saki is as light as Micuta.
Klein is heavier than Simba.In the next part of the Fluttering Dart series, we'll delve into control flow statements another Dart fundamental needed for building robust Flutter apps.
Tha(nk|t')s all!