Everyday Dart For Flutter: Part 1 cover
The Boring Flutter·#2

Everyday Dart For Flutter: Part 1

June 23, 2025·22 min read
  • dart
  • flutter
  • oop
  • collections
  • mixins
  • functional

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 assignment
  • final - set once. dart determines the value at runtime
  • const - 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 be implements-ed, not extended
  • base class - can only be extended, not implemented
  • final class - can't be extended or implemented at all
  • sealed 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

extendsimplements
inherit codeyesno
must implement everythingnoyes
limitone parentmultiple
use foris-a + shared behaviorpure 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 classmixin
can be instantiatednono
can have stateyesyes
applied viaextends, one onlywith, multiple allowed
use foris-a + shared behaviorplug-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.