Prequel
you already know why flutter exists.
dart is what you actually write in. and honestly, when most people first see it, the reaction is "why did google make yet another language." fair question.
but the more you use it, the more the design choices make sense. null safety that the compiler actually enforces. a type system that doesn't slow you down. OOP that handles every real scenario without getting complicated. once you get past the initial suspicion, you just write code.
this post covers what you'll use daily. not the full spec. the actual dart.
Chapter 1 - Variables and the Type System
dart is statically typed. the compiler knows every variable's type before your code runs. a bug that would crash your app in javascript gets caught at compile time in dart.
var, final, and const
var name = 'rakhul'; // String, can be reassigned
final age = 24; // int, set once at runtime
const pi = 3.14159; // compile-time constant
var- reassignable. type is locked in after the first assignmentfinal- set once. dart determines the value at runtimeconst- value must be known at compile time, before the app starts
interview note: people often mix up final and const. both can only be set once, but const requires the value to exist at compile time. final DateTime now = DateTime.now() is valid, const DateTime now = DateTime.now() is not. DateTime.now() runs at runtime, so const can't hold it.
a const widget in flutter is completely frozen. dart skips it during rebuilds entirely. use const on every widget that doesn't depend on dynamic data.
var vs dynamic
this is one people get wrong a lot.
var x = 42;
x = 'hello'; // error: x is an int
dynamic y = 42;
y = 'hello'; // fine
y = true; // also fine
var infers the type and locks it. dynamic turns off type checking completely. dart won't stop you from doing anything with a dynamic variable. it's like opting out of the type system. you lose autocomplete, you lose errors, you lose safety. avoid it unless you genuinely have no choice, like parsing unknown JSON.
the late keyword
sometimes you know a variable will be set before it's used, but you can't set it right away. late handles that:
late String username;
void init() {
username = fetchFromDB();
}
void show() {
print(username); // dart trusts you initialized it
}
without late, dart would complain that username might be null. with late, the compiler trusts you but throws a LateInitializationError at runtime if you access it before setting it. it's your responsibility.
explicit types
String username = 'rakhul';
int score = 100;
double ratio = 0.75;
bool isLoggedIn = true;
writing the type out and using var compile to the same thing. explicit types are useful when the right side isn't obvious at a glance.
null safety
dart has sound null safety. a type without ? literally cannot hold null. the compiler enforces this.
String name = 'rakhul'; // null is impossible here
String? nickname; // null is possible here
before null safety existed, calling a method on null would crash at runtime. now the compiler just refuses to build code that could do that. it's not a convention, it's enforced by the type system.
null operators
String? city;
String result = city ?? 'unknown'; // fallback if null
city ??= 'chennai'; // assign only if currently null
int? length = city?.length; // skip .length if city is null
?. is the null-aware access operator. if the left side is null, the whole thing short-circuits to null instead of throwing. you'll chain this constantly in flutter: user?.profile?.avatar.
the bang operator
String? value = getValue();
print(value!); // you are promising dart this is not null
! tells the compiler to trust you. if value is actually null, you get a Null check operator used on a null value crash at runtime. use it sparingly and only when you genuinely know better than the type system.
Chapter 2 - Functions
functions in dart are first-class. you can store them in variables, pass them as arguments, return them from other functions. they're just values.
basic function
int add(int a, int b) {
return a + b;
}
arrow syntax
single-expression functions can drop the braces and return:
int add(int a, int b) => a + b;
=> is everywhere in flutter. getters, callbacks, build methods. you'll write it without thinking after a while.
named parameters
void greet({required String name, int times = 1}) {
for (var i = 0; i < times; i++) print('hello, $name');
}
greet(name: 'rakhul');
greet(name: 'rakhul', times: 3);
{ } makes parameters named. required forces the caller to pass it. giving a default value makes it optional. every flutter widget constructor works this way.
positional optional parameters
there's another flavor: positional optional parameters, wrapped in [ ]:
void log(String message, [String? level]) {
print('[${level ?? 'info'}] $message');
}
log('server started'); // [info] server started
log('disk full', 'warning'); // [warning] disk full
these are optional but not named. caller decides by position, not by name. interview note: the difference between { } and [ ] parameters comes up often. named { } can be in any order when called. positional [ ] must follow the order they're defined. flutter widgets use named params almost exclusively.
functions as values
int operate(int a, int b, int Function(int, int) op) {
return op(a, b);
}
operate(5, 3, (a, b) => a + b); // 8
operate(5, 3, (a, b) => a * b); // 15
int Function(int, int) is the type signature of a function that takes two ints and returns one. when you write onPressed: () => doSomething() in flutter, that's you passing a function as a value.
Chapter 3 - Conditionals
if / else
int score = 85;
if (score >= 90) {
print('A');
} else if (score >= 80) {
print('B');
} else {
print('C or below');
}
nothing special here. works exactly like every other language.
ternary operator
when you need a value based on a condition, the ternary is cleaner than a full if/else:
var score = 87;
var grade = score >= 90 ? 'A' : 'B'; // 'B'
condition ? valueIfTrue : valueIfFalse. one line, one value. use it for simple yes/no decisions.
switch statement
var day = 'monday';
switch (day) {
case 'saturday':
case 'sunday':
print('weekend');
break;
case 'monday':
print('back to work');
break;
default:
print('weekday');
}
use switch when you are checking one variable against multiple values. each case needs a break or the code falls through to the next case. default catches everything else.
switch expression (dart 3)
dart 3 added a switch that produces a value instead of executing statements:
var score = 87;
var grade = switch (score) {
>= 90 => 'A',
>= 80 => 'B',
>= 70 => 'C',
_ => 'F',
};
_ is the catch-all. no break. the whole thing is one expression.
interview note: switch statement executes code, switch expression produces a value. the expression version has no fall-through by design. the really interesting thing: when used with sealed classes (dart 3+), the compiler checks that every possible subtype is handled. if you miss one, it's a compile error. this is called exhaustiveness checking and it makes refactoring sealed class hierarchies very safe.
Chapter 4 - Loops
classic for loop
for (var i = 0; i < 5; i++) {
print(i); // 0, 1, 2, 3, 4
}
three parts: setup, condition, step. runs as long as the condition is true.
for-in loop
when you just want to go through every item in a collection, for-in is cleaner:
var fruits = ['apple', 'mango', 'banana'];
for (var fruit in fruits) {
print(fruit);
}
for-in works on any Iterable. lists, sets, maps, whatever. you don't need an index counter.
forEach
forEach is the functional style of doing the same thing:
var fruits = ['apple', 'mango', 'banana'];
fruits.forEach((fruit) => print(fruit));
same result, different style. forEach takes a function and calls it for each item.
for-in vs forEach: this comes up in interviews. the key difference is control flow. break and continue work in for-in. they do nothing useful in forEach. there's also an async trap: if you use await inside a forEach callback, it doesn't do what you expect. dart fires all the callbacks and doesn't wait. for async iteration, use a regular for-in loop.
var numbers = [1, 2, 3, 4, 5];
for (var n in numbers) {
if (n == 3) break; // stops the loop
if (n == 2) continue; // skips this iteration
print(n); // prints 1
}
while loop
runs as long as a condition is true. checks the condition before each run:
var count = 0;
while (count < 3) {
print(count); // 0, 1, 2
count++;
}
use while when you don't know ahead of time how many times you need to loop.
do-while loop
checks the condition after each run, so it always runs at least once:
var count = 0;
do {
print(count); // 0, 1, 2
count++;
} while (count < 3);
the practical difference: if count started at 10, while would never run but do-while would run once. use do-while when the first run should always happen regardless of the condition.
Chapter 5 - Collections
dart has three main collection types: List, Set, and Map.
List
ordered, index-based, allows duplicates.
List<String> fruits = ['apple', 'mango', 'banana'];
fruits.add('grape');
fruits.remove('mango');
fruits[0]; // 'apple'
fruits.length; // 3
fruits.contains('banana'); // true
think of List as an array with a proper API.
nuance: List in dart is growable by default. but List.filled(3, 0) creates a fixed-length list. calling add() on it throws a runtime error. this trips people up when they create a list for performance and then try to append to it.
spread operator
you can insert all items from one list into another using ...:
var base = ['react', 'vue'];
var all = ['angular', ...base, 'svelte'];
// ['angular', 'react', 'vue', 'svelte']
List<String>? maybe;
var safe = [...?maybe, 'flutter']; // ...? only spreads if not null
collection if and collection for
dart lets you put if and for directly inside a list literal:
bool isLoggedIn = true;
var items = [
'home',
if (isLoggedIn) 'profile',
'settings',
];
// ['home', 'profile', 'settings']
var doubled = [for (var i in [1, 2, 3]) i * 2];
// [2, 4, 6]
this is one of the best things in dart. in flutter, you build widget trees like this:
Column(
children: [
Text('always here'),
if (user != null) UserCard(user: user!),
for (var item in items) ListTile(title: Text(item)),
],
)
no helper functions, no extra variables. it just works inline.
Set
unordered, no duplicates.
Set<String> tags = {'dart', 'flutter', 'mobile'};
tags.add('dart'); // no effect, already exists
tags.length; // 3
use Set when you only care about whether something is in the collection, not where it is.
Map
key-value pairs.
Map<String, int> scores = {
'rakhul': 95,
'anon': 80,
};
scores['rakhul']; // 95
scores['new'] = 100; // add entry
scores.remove('anon'); // remove entry
scores.containsKey('rakhul'); // true
scores.entries; // Iterable of MapEntry
maps power most of what you build. api responses, state objects, configs.
nuance worth knowing: the [] operator on a Map always returns a nullable type. scores['rakhul'] returns int?, not int, even though you know that key exists. dart can't prove it at compile time, so it's always nullable. either null-check it or use scores['rakhul']! if you're sure.
Chapter 6 - OOP Fundamentals
dart is fully object-oriented. everything is an object, including numbers and functions.
class and constructor
class Person {
String name;
int age;
Person(this.name, this.age);
}
var p = Person('rakhul', 24);
print(p.name); // rakhul
this.name in the constructor is shorthand for "take this parameter and write it to the field with the same name." that's just the dart way.
named constructors
dart doesn't support method overloading. you can't have two methods named greet() with different signatures in the same class. named constructors are the answer:
class Point {
double x, y;
Point(this.x, this.y);
Point.origin() : x = 0, y = 0;
Point.fromMap(Map<String, double> map)
: x = map['x'] ?? 0,
y = map['y'] ?? 0;
}
var origin = Point.origin();
var p = Point.fromMap({'x': 3.0, 'y': 4.0});
the : x = 0, y = 0 after () is an initializer list. it runs before the constructor body, which matters when fields are final. SomeClass.from(), SomeClass.empty(), SomeClass.of(context) in flutter are all named constructors.
const constructors
if all fields are final, you can make the constructor const:
class Color {
final int r, g, b;
const Color(this.r, this.g, this.b);
}
const red = Color(255, 0, 0); // compile-time constant
this is how flutter widgets work. const Text('hello') creates a compile-time constant widget that dart can cache and reuse. the rule: for a constructor to be const, every field must be final and every field's value must itself be computable at compile time.
factory constructors
a regular constructor always creates a new instance. a factory constructor doesn't have to:
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() => _instance; // returns cached instance
Logger._internal();
}
var a = Logger();
var b = Logger();
print(identical(a, b)); // true, same object
factory constructors are used for singletons, caching, or returning a subtype. fromJson factory constructors are everywhere in flutter apps:
factory User.fromJson(Map<String, dynamic> json) {
return User(name: json['name'], age: json['age']);
}
getters and setters
class Circle {
double radius;
Circle(this.radius);
double get area => 3.14159 * radius * radius;
set diameter(double d) => radius = d / 2;
}
var c = Circle(5);
print(c.area); // 78.53975, accessed like a property
c.diameter = 20; // calls the setter, sets radius to 10
a getter looks like a property from the outside. no parentheses. callers don't know if they're reading a stored field or a computed value. that's by design.
private fields
class BankAccount {
double _balance = 0; // _ prefix makes it private to this file
double get balance => _balance;
void deposit(double amount) {
if (amount > 0) _balance += amount;
}
}
in dart, _ makes something private to its library, which in practice means the file. not just the class, the whole file. two classes in the same file can access each other's _ members. this surprises people coming from java or kotlin where private is scoped to the class itself.
Chapter 7 - Inheritance
inheritance is the "is-a" relationship. a Dog is an Animal. a Button is a Widget.
class Animal {
String name;
Animal(this.name);
void speak() => print('$name makes a sound');
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
void speak() => print('$name barks');
}
var d = Dog('Bruno');
d.speak(); // Bruno barks
extends gives the child class everything the parent has. @override signals intent to replace a parent method. super(name) calls the parent constructor and must be done before the constructor body runs.
runtime polymorphism
this is one of the most important OOP concepts and it's built into how dart works:
Animal a = Dog('Rex'); // variable type is Animal
a.speak(); // which speak() runs?
at compile time, a is typed as Animal. but at runtime, dart looks at what a actually holds, which is a Dog, and calls Dog's speak(). this is runtime polymorphism, also called dynamic dispatch. the actual method that runs is decided at runtime based on the real object, not the declared type.
this is why you can write:
List<Animal> animals = [Dog('Rex'), Cat('Whiskers')];
for (var a in animals) {
a.speak(); // each animal speaks in its own way
}
you don't know or care what type each element actually is. dart figures it out.
what about compile-time polymorphism?
compile-time polymorphism traditionally means method overloading: same function name, different parameter types, compiler picks the right one. dart does not support this. you cannot have two methods named greet() in the same class with different signatures.
dart solves the same problem with named and optional parameters instead:
// invalid in dart:
void greet(String name) {}
void greet(String name, int age) {} // error
// dart's approach:
void greet(String name, [int? age]) {}
// or
void greet({required String name, int? age}) {}
interview note: if someone asks "does dart support compile-time polymorphism," the honest answer is: not through overloading, because dart doesn't allow it. but it achieves similar flexibility through optional/named parameters. generics are also a form of compile-time polymorphism.
is and as
Animal a = Dog('Rex');
if (a is Dog) {
a.speak(); // dart narrows the type here automatically
}
var d = a as Dog; // forced cast, throws if the type is wrong at runtime
is does a type check and smart-casts inside the block. no manual cast needed. as is a hard cast, if you're wrong, CastError at runtime. use is when you're not sure. use as only when you are.
Chapter 8 - Abstract Classes and Interfaces
abstract class
an abstract class cannot be created directly. it only exists to be extended.
abstract class Shape {
double get area; // subclasses must provide this
void draw(); // subclasses must provide this
void describe() => print('area: ${area.toStringAsFixed(2)}');
}
class Rectangle extends Shape {
double width, height;
Rectangle(this.width, this.height);
@override
double get area => width * height;
@override
void draw() => print('rect ${width}x${height}');
}
Shape defines what must exist. Rectangle fills in how it works. describe() is shared logic that every subclass inherits automatically.
Widget, State, RenderObject in flutter are all abstract. you extend them, you never instantiate them.
nuance: in dart 3, there are new class modifiers that give you more control:
interface class- can only beimplements-ed, not extendedbase class- can only be extended, not implementedfinal class- can't be extended or implemented at allsealed class- like abstract, but the compiler knows all subclasses and enables exhaustiveness checking in switch
these are advanced, but good to know exist.
implements
dart has no interface keyword. any class can be used as an interface:
class Logger {
void log(String message) => print(message);
}
class DatabaseLogger implements Logger {
@override
void log(String message) => print('[DB] $message');
}
implements is a contract. you're saying "i guarantee i have every public member of this class." you get zero shared implementation. you write every method yourself from scratch.
extends vs implements
extends | implements | |
|---|---|---|
| inherit code | yes | no |
| must implement everything | no | yes |
| limit | one parent | multiple |
| use for | is-a + shared behavior | pure contract |
you can combine: class Car extends Vehicle implements Serializable. the class extends one parent and satisfies one or more contracts.
interview note: in most OOP-heavy interviews, they'll ask when to use extends vs implements. the cleaner answer: extends when you want shared code and an is-a relationship. implements when you only care about enforcing the shape of a class, not inheriting its behavior. dart's implements is stricter than many languages because there's no default method inheritance.
Chapter 9 - Mixins
a mixin is a chunk of methods you can add to a class without making it a parent. think of it as a plugin you attach.
mixin Flyable {
void fly() => print('$runtimeType is flying');
}
mixin Swimmable {
void swim() => print('$runtimeType is swimming');
}
class Duck extends Animal with Flyable, Swimmable {
Duck() : super('duck');
}
var d = Duck();
d.fly(); // Duck is flying
d.swim(); // Duck is swimming
d.speak(); // duck makes a sound
with applies the mixin. multiple mixins are fine. Duck is not a Flyable, it just has the fly behavior bolted on.
before mixins, you might ask: why not just extend both Flyable and Swimmable as classes? the answer is dart doesn't allow it. dart has no multiple inheritance. you can only extend one class. and this is a deliberate choice to avoid the diamond problem.
the diamond problem is what happens when two parent classes both define the same method, and a child inherits from both. which version does the child get? the compiler genuinely doesn't know. the name refers to the shape of the inheritance diagram — A at the top, B and C both inherit from A, D inherits from both B and C — a diamond. if B and C both override a method from A, D is stuck in an ambiguous situation.
dart sidesteps this entirely by disallowing extending more than one class. but you still need shared behavior across unrelated class trees — that's exactly what mixins give you. a mixin isn't a parent. it's applied behavior. and when two mixins conflict on the same method name, dart has a defined rule: last one in the with clause wins.
mixin conflict: what if two mixins have the same method?
this is a real interview question. say both Flyable and Swimmable have a method called move():
mixin Flyable {
void move() => print('flying');
}
mixin Swimmable {
void move() => print('swimming');
}
class Duck extends Animal with Flyable, Swimmable {
Duck() : super('duck');
}
Duck().move(); // swimming
the last mixin in the with clause wins. Swimmable is applied after Flyable, so its move() takes over. this is dart's mixin linearization.
the resolution order is: the class itself, then last mixin first, going backwards to the first mixin, then the parent. so for Duck with Flyable, Swimmable, the chain is: Duck > Swimmable > Flyable > Animal. whichever definition is found first in that chain wins.
if you want to explicitly pick one, override in the class:
class Duck extends Animal with Flyable, Swimmable {
Duck() : super('duck');
@override
void move() => print('duck can do both, picks flying');
}
restricting with on
mixin Logger on Widget {
void log(String msg) => print('[${runtimeType}] $msg');
}
on Widget restricts the mixin to classes that extend Widget. inside the mixin, you can safely call widget-only methods and access widget properties because dart knows the host class has them.
mixins vs abstract classes
| abstract class | mixin | |
|---|---|---|
| can be instantiated | no | no |
| can have state | yes | yes |
| applied via | extends, one only | with, multiple allowed |
| use for | is-a + shared behavior | plug-in behavior |
TickerProviderStateMixin and AutomaticKeepAliveClientMixin in flutter are mixins. they plug in animation and scroll-preservation capabilities without requiring your state class to inherit from anything new.
nuance: mixins can have fields (state). but they can't have constructor parameters. if a mixin needs configuring, it usually exposes abstract getters that the host class provides.
mixin Validator {
int get maxLength; // host class must provide this
bool isValid(String input) => input.length <= maxLength;
}
class NameField with Validator {
@override
int get maxLength => 50;
}
Chapter 10 - Functional Operations
dart's Iterable API lets you process collections without manual loops. each method does one clear job.
forEach
calls a function for every element. doesn't return anything:
var fruits = ['apple', 'mango', 'banana'];
fruits.forEach((f) => print(f));
async trap: forEach does not handle async callbacks the way you'd expect. if your callback is async, dart fires them all off and moves on without waiting. use for-in if you need to await inside the loop.
map - transform every element
creates a new collection by running a function on each item:
var scores = [85, 92, 78, 95];
var grades = scores.map((s) => s >= 90 ? 'A' : 'B').toList();
// ['B', 'A', 'B', 'A']
map is lazy. it returns a MappedListIterable, not a List. nothing actually runs until you call .toList() or iterate. this matters in loops: if you call map and never consume the result, no work happens.
where - filter elements
keeps only the items that pass the test:
var scores = [85, 92, 78, 95];
var passing = scores.where((s) => s >= 90).toList();
// [92, 95]
where is dart's filter. it does not change the original list.
reduce - collapse to one value
folds the whole list into a single value:
var scores = [85, 92, 78, 95];
var total = scores.reduce((sum, s) => sum + s);
// 350
reduce needs at least one element in the list or it throws. the first call uses the first element as the starting accumulator.
fold - reduce with a starting value
safer version of reduce. you provide the starting value yourself:
var scores = [85, 92, 78, 95];
var total = scores.fold<int>(0, (sum, s) => sum + s);
// 350, works even on an empty list
interview note: reduce vs fold is asked often. the real difference: reduce uses the first element as the starting accumulator, so calling it on an empty list throws a StateError. fold takes an initial value you provide, so it works safely on empty lists. prefer fold in production code.
any and every
var scores = [85, 92, 78, 95];
scores.any((s) => s > 90); // true, at least one is above 90
scores.every((s) => s > 70); // true, all are above 70
firstWhere
find the first item that matches:
scores.firstWhere((s) => s > 90);
// 92
scores.firstWhere((s) => s > 99, orElse: () => 0);
// 0, orElse runs when nothing matches instead of throwing
chaining
all these operations return Iterable, so you can chain them:
var result = [1, 2, 3, 4, 5, 6]
.where((n) => n.isEven)
.map((n) => n * n)
.toList();
// [4, 16, 36]
each step adds to a lazy chain. nothing actually runs until .toList(), .forEach(), or any other terminal method is called. this is why iterating a where().map() chain doesn't process anything until you force it.
Chapter 11 - Shorthands and Cascades
string interpolation
var name = 'rakhul';
var score = 95;
print('hello, $name'); // hello, rakhul
print('score: ${score * 2}'); // score: 190
$variable for a direct reference. ${expression} when you need to compute something.
cascade operator
.. lets you call multiple methods on the same object without repeating it:
// without cascade
var list = [];
list.add(1);
list.add(2);
list.add(3);
// with cascade
var list = []
..add(1)
..add(2)
..add(3);
each .. call returns the original object, not the result of the method. you will see this with Paint and StringBuffer in flutter.
null-aware cascade
StringBuffer? buffer;
buffer?..write('hello')..write(' world');
// does nothing if buffer is null
?.. is the null-safe version. the whole chain is skipped if the object is null.
records
dart 3 added records, which let you return multiple values without creating a class:
(String, int) getUser() => ('rakhul', 24);
var (name, age) = getUser();
print('$name is $age'); // rakhul is 24
before records, you had to either make a wrapper class or return a list and cast everything. records solve that cleanly.
typedef
typedef OnMessage = void Function(String message);
void runWithCallback(OnMessage cb) {
cb('done');
}
typedef gives a name to a function signature. without it, complex function types like void Function(String, int, bool) get hard to read fast.
Post-Credits
that's the dart you actually need.
variables, null safety, functions, conditionals, loops, collections, OOP, runtime polymorphism, mixins, functional operations, shorthands, cascades. all of it, in one post.
if you understand what's on this page, you're not learning dart anymore. you're writing it.
next up: async dart. futures, streams, and how dart handles things that take time.
stay tuned.
