Lesson 1 · Dart Language

Speaking to the Machine with Dart

Before we can build a beautiful app, we must learn the language it is written in. Flutter's language is Dart, created by Google and designed to be clean, readable and beginner-friendly. Do not be intimidated by the word "code." Code is simply logic written down. If you can follow a recipe to bake a cake, you already have the kind of thinking that programming needs.

Every Program Starts at main

When you run a Dart program, the computer looks for one special starting point: a function called main. Everything begins there, just like a book begins on page one. Here is the traditional first program every developer on earth writes — printing a message to the screen:

void main() { print('Hello, World!'); }

Your very first program. Running it displays: Hello, World!

Let us slowly unpack every piece, because understanding this small snippet deeply is worth more than skimming ten big ones:

Precision matters: Computers are painfully literal. Forget one semicolon, mismatch a bracket, or misspell print as Print, and the program will refuse to run. This feels harsh at first, but it trains a valuable habit: care and attention to detail.
Comments are notes for humans that the computer ignores completely. Write them with //. Use them generously to explain why your code does something — future-you will be grateful.

Deep Dive: JIT vs AOT Compilation

What makes Dart special for Flutter is its dual-compilation capability. During development, Dart uses JIT (Just-In-Time) compilation. This allows for Flutter's famous "Hot Reload" — you change the code, and it updates on the screen in milliseconds because it compiles on the fly. However, when you are ready to publish your app, Dart uses AOT (Ahead-Of-Time) compilation. This translates your code directly into native machine code (like ARM for iPhones or Androids), making your app run blisteringly fast without any translation overhead during execution.

Animated Example

Your first program runs and prints

void main() { print('Hello, World!');}
> Hello, World!
  1. Every Dart program starts at main(). Execution reaches the print line.
  2. print(...) sends its text to the screen — and out comes Hello, World!

Key Takeaways

  • Every Dart program starts running from the main() function.
  • print(...) displays text; strings are text wrapped in quotes.
  • Each statement ends with a semicolon ; — like a full stop.
  • Comments with // are notes for humans that the computer ignores.

Your Turn: Practice

Write (on paper or in an online Dart pad like dartpad.dev) a main function that prints your name on one line and your favourite hobby on the next. Add a comment above each line explaining what it does.

Lesson 2 · Dart Language

Variables: Boxes That Remember

The first thing any app needs to do is remember things. What is the user's name? What is their score? Is dark mode on? We store these values in variables. Think of a variable as a labelled cardboard box: you write a name on the outside, and you place a value inside.

// A box named 'userName' that holds text String userName = 'Alex'; // A box named 'age' that holds a whole number int age = 25; // A box named 'isLoggedIn' that holds true or false bool isLoggedIn = true;

Three variables, three different types of data.

The Essential Data Types

Notice the word before each variable name — String, int, bool. That is the type: it tells Dart exactly what kind of value lives in the box. These are the types you will use constantly:

Why do types matter so much?

Imagine trying to do maths by adding the word "Apple" to the number 5. It is nonsense. By forcing you to declare what lives in each box, Dart can catch that nonsense before your app ships to users. Types are a safety net: they turn a crash that would have annoyed a real user into a friendly warning while you are still writing. This is why Dart is called a "type-safe" language, and it is one of its greatest strengths.

var, final and const

You have three more handy keywords for creating variables:

var city = 'Cairo'; // Dart infers this is a String final birthYear = 1998; // set once, never changes const pi = 3.14159; // a fixed, known-forever value

The late Keyword

Sometimes you want to declare a variable but you can't give it a value immediately (e.g., waiting for data from the internet). You can use the late keyword to tell Dart: "I promise I will give this a value before I use it."

late String serverResponse; // ... later in the code ... serverResponse = 'Data loaded!';

Be careful: if you break that promise and try to use it before assigning a value, your app will crash!

Good habit: Prefer final by default, and only use a changeable variable when you truly need the value to change later. Code that changes less is code with fewer surprises — and fewer bugs.

Deep Dive: The dynamic Type

Sometimes you genuinely do not know what type of data you will receive (e.g., parsing a complex JSON response). Dart offers the dynamic keyword, which disables type checking for that variable. It can be a String now, and an int later.

dynamic mystery = 'Hello'; mystery = 42; // Perfectly legal, no error

However, use dynamic as a last resort! It completely removes Dart's safety net. If you try to call a String method on mystery while it holds a number, your app will crash at runtime. Prefer explicitly defining types whenever possible to keep your codebase robust.

Deep Dive: late and Flutter Controllers

In Flutter, you often create controllers (like TextEditingController or AnimationController) that need access to the widget's context, which isn't available at the exact moment the variable is created. The late keyword is the professional's solution: it promises Dart the variable will be initialized in the initState() method before it's ever drawn on screen.

late AnimationController _controller; void initState() { super.initState(); _controller = AnimationController(vsync: this); // Safe because it's late }
Animated Example

Values being stored in named boxes

String name"Alex"
int age25
bool isCooltrue
  1. A box named name of type String stores the text "Alex".
  2. A box age of type int holds the whole number 25.
  3. A box isCool of type bool holds true. Each type guards what can go inside.

Key Takeaways

  • Variables are named boxes that store values so your app can remember them.
  • The core types are String (text), int (whole numbers), double (decimals) and bool (true/false).
  • Types are a safety net: they catch nonsense mistakes before users ever see them.
  • Prefer final for values that should not change; use const for fixed constants.

Your Turn: Practice

Declare four variables that describe you: a String for your name, an int for your age, a double for your height in metres, and a bool for whether you like coffee. Choose the correct type for each — getting the types right is the whole point.

Lesson 3 · Dart Language

Doing Things with Values: Operators

Storing values is useful, but the real power comes from combining and comparing them. We do that with operators — small symbols that perform an action on your values. You already know most of them from school maths.

Arithmetic Operators

int a = 10; int b = 3; print(a + b); // 13 (addition) print(a - b); // 7 (subtraction) print(a * b); // 30 (multiplication) print(a / b); // 3.333... (division, gives a double) print(a % b); // 1 (remainder — what is left over)

That last one, % (called "modulo"), is more useful than it looks. It gives the remainder after division. A classic trick: number % 2 == 0 is true only for even numbers — perfect for "colour every other row" style logic.

Comparison Operators (they produce true or false)

These compare two values and hand back a boolean. They are the raw material of every decision an app makes:

Logical Operators (combining conditions)

Often you need to check more than one thing at once. Logical operators join booleans together:

bool hasTicket = true; int age = 20; // Allowed in only if they have a ticket AND are 18 or older bool canEnter = hasTicket && age >= 18; // true

String Interpolation — Putting Values Into Text

You will constantly need to slot a variable's value into a sentence. Dart makes this beautifully clean with the $ symbol, a technique called string interpolation:

String name = 'Sara'; int score = 95; print('Hi \$name, your score is \$score!'); // Output: Hi Sara, your score is 95! // For a calculation, wrap it in curly braces: print('Next year you will be \${score + 5}.');

$ inserts a variable; ${ } inserts a whole expression.

Remember: a single = assigns a value (puts something in the box), while a double == compares two values. Mixing them up is the single most common beginner mistake — now you will not make it.

Deep Dive: The Cascade Operator (..)

Dart has a unique operator called the cascade operator (..). It allows you to perform a sequence of operations on the same object without having to repeatedly type the object's name. It's incredibly useful when setting up complex UI components in Flutter.

// Without cascade: Paint paint = Paint(); paint.color = Colors.black; paint.strokeWidth = 5.0; // With cascade (cleaner and more readable): Paint paint = Paint() ..color = Colors.black ..strokeWidth = 5.0;

Deep Dive: Null-Aware Operators

Dart shines with operators designed specifically to handle missing data (nulls) safely without crashing. The null-aware assignment ??= only assigns a value if the variable is currently null. The null-aware spread operator ...? adds a list into another list only if it actually exists.

String? userName; userName ??= 'Guest'; // Assigns 'Guest' because userName is null List<String>? extraItems; List<String> menu = ['Home', ...?extraItems]; // Safely ignores extraItems if null
Animated Example

Operators combining and comparing values

> 2 + 3 * 414 // * before +
> 'Ria' + ' Academy''Ria Academy'
> 10 == '10'false // number ≠ text
> age >= 18 && hasTickettrue
  1. Maths follows order of operations: * before +, giving 14.
  2. The + operator also joins strings together.
  3. == compares value and type — a number is never equal to text.
  4. && is true only when both sides are true — the core of real decisions.

Key Takeaways

  • Arithmetic operators (+ - * / %) do maths; % gives the remainder.
  • Comparison operators (==, !=, >, <) produce a true/false result.
  • Logical operators combine conditions: && (and), || (or), ! (not).
  • String interpolation with $ slots variables neatly into text.

Your Turn: Practice

Using string interpolation, write a line that prints: "In 5 years, [name] will be [age + 5] years old." using variables from the last lesson. Remember to wrap the calculation in ${ }.

Lesson 4 · Dart Language

Making Decisions with If / Else

An app that always does the exact same thing no matter what would be useless. Apps must react — to a tap, a login, a value. The tool that lets code make a choice is the if statement, and it maps almost perfectly onto everyday English.

The Basic if

It reads exactly how it sounds: "IF this condition is true, THEN run this block of code."

int userAge = 20; if (userAge >= 18) { print('Welcome to the app!'); }

The part in the round brackets — userAge >= 18 — is the condition. It always boils down to true or false. If it is true, the code in the curly braces runs. If it is false, that block is simply skipped.

Adding an else

Usually you also want a plan for when the condition is false. That is what else is for: "OTHERWISE, do this instead."

int userAge = 16; if (userAge >= 18) { print('Welcome to the app!'); } else { print('Sorry, you must be 18 or older.'); }

Every login screen you have ever used runs this exact pattern behind the scenes: if the password is correct, open the app; else, show an error.

Multiple Paths with else if

When there are several possibilities, chain them together with else if. Dart checks each condition from top to bottom and runs the first one that is true:

int score = 82; if (score >= 90) { print('Grade: A'); } else if (score >= 80) { print('Grade: B'); // this one runs } else if (score >= 70) { print('Grade: C'); } else { print('Grade: F'); }

The switch Statement

When you are checking one variable against many exact values, a switch can be cleaner than a long chain of else ifs:

String day = 'Sunday'; switch (day) { case 'Saturday': case 'Sunday': print('Weekend!'); break; default: print('A weekday.'); }
Think in conditions. Almost every interactive feature you will ever build — showing a badge, unlocking a level, validating a form — is at heart just an if statement asking a yes/no question. Master this and you can express almost any app behaviour.

Deep Dive: The Ternary Operator (? :)

Often, you need an if/else just to decide which value to assign to a variable. The ternary operator condenses this into a single, elegant line. It asks a question (?), provides the true answer, then a colon (:), and the false answer.

int age = 20; // Long way: String status1; if (age >= 18) { status1 = 'Adult'; } else { status1 = 'Minor'; } // The Ternary way: condition ? ifTrue : ifFalse String status2 = age >= 18 ? 'Adult' : 'Minor';

You will use this constantly in Flutter to show one widget or another based on state (e.g., isLoading ? LoadingSpinner() : UserProfile()).

Deep Dive: Pattern Matching in Dart 3

Dart 3 introduced massive upgrades to logic flow with Pattern Matching. It allows you to deeply inspect the shape and contents of data inside a switch statement or an if-case. It replaces messy chains of if statements with elegant, declarative logic.

dynamic jsonData = ['user', 'Alex', 25]; if (jsonData case ['user', String name, int age]) { print('Valid user: \$name, Age: \$age'); } else { print('Invalid data format'); }

The case checks if the list has exactly 3 items, starts with 'user', and binds the next two items directly to variables name and age. This is a game-changer for parsing JSON.

Animated Example

The same condition, two different results

age = ?
✓ Access granted
✗ Access denied
  1. First run: the user's age is 20.
  2. 20 >= 18 is true, so the if block runs — access granted.
  3. Second run: age is 15. 15 >= 18 is false, so the else runs — denied.

Key Takeaways

  • if runs a block only when its condition is true; else handles the false case.
  • Chain several possibilities with else if — the first true one wins.
  • A switch can be cleaner when checking one value against many exact options.
  • Nearly every interactive feature is, at heart, an if asking a yes/no question.

Your Turn: Practice

Write an if / else if / else that takes a variable temperature and prints "Cold" below 15, "Warm" between 15 and 30, and "Hot" above 30. Test it in your head with the values 10, 22 and 35.

Lesson 5 · Dart Language

Loops: Doing Things Over and Over

Computers are extraordinary at repetition — they can do the same task a million times without getting bored or making a mistake. When you want to repeat an action, you use a loop. Imagine you need to display 100 messages in a chat: you would never copy-paste code 100 times. You write it once inside a loop.

The for Loop

The for loop is perfect when you know how many times you want to repeat. It has three parts packed into its brackets: a starting point, a condition to keep going, and a step that runs each round.

for (int i = 1; i <= 5; i++) { print('This is line number \$i'); } // Prints line 1, 2, 3, 4, then 5, and stops.

Reading it piece by piece:

So i becomes 1, 2, 3, 4, 5 — and when it reaches 6, the condition 6 <= 5 is false, so the loop stops. Elegant and precise.

The while Loop

Sometimes you do not know the exact number of repeats in advance — you just want to keep going until some condition changes. That is the while loop:

int lives = 3; while (lives > 0) { print('You have \$lives lives left.'); lives = lives - 1; // eventually reaches 0 and stops }
The infinite loop trap: A while loop must eventually make its condition false, or it will run forever and freeze your app. In the example above, if we forgot the line lives = lives - 1, lives would stay 3 and the loop would never end. Always make sure something inside the loop moves it toward stopping.

The for-in Loop — Going Through a List

The most common loop in real apps walks through a collection of items one by one. You will use this constantly to display lists of posts, products or messages:

List<String> fruits = ['Apple', 'Banana', 'Grape']; for (String fruit in fruits) { print('I like \$fruit'); } // I like Apple / I like Banana / I like Grape

We will meet Lists properly in the next lesson — but notice how naturally the loop reads: "for each fruit in fruits, print it."

The rule of thumb: use a for loop when you know the count, a while loop when you are waiting for a condition, and a for-in loop when you are stepping through a collection.

Deep Dive: break and continue

You can control the flow inside a loop using two special keywords:

for (int i = 1; i <= 5; i++) { if (i == 2) { continue; // Skips the rest of this round (won't print 2) } if (i == 4) { break; // completely stops the loop } print(i); } // Output: 1, 3
Animated Example

A for-loop counting 1 to 5

1
2
3
4
5
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
  1. i starts at 1. Condition 1 <= 5 is true → print "Count: 1".
  2. i++ makes it 2. Still ≤ 5 → run again.
  3. 3 — the loop keeps going, printing each round.
  4. 4 — one more pass.
  5. 5 is the last valid value. Next i is 6; 6 <= 5 is false, so the loop stops.

Key Takeaways

  • Loops repeat work so you never copy-paste the same code.
  • Use a for loop when you know the number of repeats.
  • Use a while loop when you repeat until a condition changes — and always make it eventually stop.
  • Use a for-in loop to step through every item in a collection.

Your Turn: Practice

Write a for loop that prints the numbers 1 to 10, but for every even number prints "[number] is even" instead. Hint: combine a loop with an if using the % operator.

Lesson 6 · Dart Language

Functions: Package Your Logic

Imagine you have ten lines of code that calculate the total price of a shopping cart. If the user checks out from five different screens, you certainly do not want to copy those ten lines five times. Instead, you bundle them together, give them a name, and reuse them anywhere with a single word. That bundle is a function.

The Factory Analogy

A function is a little factory. You hand it raw materials (called parameters or inputs), it does some work inside, and it hands you back a finished product (the return value). Build the factory once; use it a thousand times.

Defining and Calling a Function

// Define the factory: takes two numbers, returns their sum int calculateTotal(int item1, int item2) { int total = item1 + item2; return total; // hand the result back } // Use (call) it anytime you like int myBill = calculateTotal(50, 20); print(myBill); // 70

Look closely at the definition. The int at the very front is the return type — it promises the function hands back a whole number. Inside the brackets, int item1, int item2 are the parameters — the inputs the factory needs. The return keyword sends the finished value back to whoever called it.

Functions That Return Nothing

Not every function produces a value; some just do something, like showing a message. Those use the return type void, which means "returns nothing":

void greetUser(String name) { print('Welcome back, \$name!'); } greetUser('Sara'); // Welcome back, Sara!

The Short "Arrow" Syntax

When a function is just a single line, Dart offers a lovely shorthand using => (called the "fat arrow"). These two functions do exactly the same thing:

// Long form int double1(int x) { return x * 2; } // Short arrow form — identical result int double2(int x) => x * 2;

You will see the arrow form everywhere in Flutter, especially for button actions, so it is worth getting comfortable with now.

Named & Optional Parameters

When a function has many inputs, it can be hard to remember their order. Dart lets you use named parameters by wrapping them in curly braces { }. You can also make them optional or give them default values:

void buildProfile({required String name, int age = 18}) { print('Profile: \$name, Age: \$age'); } // The order no longer matters! buildProfile(age: 25, name: 'Alex');

Anonymous Functions

Sometimes you need a function just once, right where you are typing. You don't need to give it a name. These are called anonymous functions or lambdas:

List<String> items = ['apple', 'banana']; items.forEach((item) { print(item.toUpperCase()); });

Notice the (item) { ... } part? That's a nameless function passed directly into another method.

The golden principle: DRY

Programmers live by a rule called DRY — "Don't Repeat Yourself." If you ever notice you are copy-pasting the same code, that is a clear signal it wants to become a function. Functions keep your app small, readable, and easy to fix: change the logic in one place, and every part of your app that uses it updates instantly.

Deep Dive: Functions as First-Class Citizens

In Dart, functions are "first-class citizens". This means you can treat a function exactly like a variable: you can pass a function as an argument to another function, return a function from a function, or assign a function to a variable.

void executeTwice(Function action) { action(); action(); } void sayHello() => print('Hello!'); executeTwice(sayHello); // Prints 'Hello!' twice

This is incredibly powerful in Flutter, where you frequently pass functions (callbacks) to buttons to tell them what to do when pressed.

Deep Dive: Lexical Closures

A closure is a function object that has access to variables in its lexical scope, even when the function is used outside of its original scope. Simply put, a function "remembers" the environment in which it was created.

Function makeAdder(int addBy) { return (int i) => addBy + i; // Returns an anonymous function } void main() { // add2 "remembers" that addBy is 2 var add2 = makeAdder(2); // add5 "remembers" that addBy is 5 var add5 = makeAdder(5); print(add2(3)); // 5 print(add5(3)); // 8 }

Closures allow you to create highly flexible factory functions and are extensively used when managing state and delayed executions.

Deep Dive: Tear-offs

If you have a function that takes the exact same arguments as the function you want to pass it to, you don't need to write a new anonymous function. You can just pass the function's name. This is called a tear-off and it makes Flutter UI code much cleaner.

List<String> names = ['alex', 'sara']; // Instead of: names.forEach((name) => print(name)); names.forEach(print); // This is a tear-off! // In Flutter, instead of: ElevatedButton(onPressed: () => submitForm(), ...) ElevatedButton(onPressed: submitForm, child: Text('Submit'));
Animated Example

Input → function → output

5
10
add(a, b)
return a + b
?
  1. We call the function with two inputs: add(5, 10).
  2. Inside, the parameters become a = 5 and b = 10; it runs a + b.
  3. It returns 15 — the output. Reuse it with any inputs, forever.

Key Takeaways

  • A function bundles reusable logic under a name: build once, use anywhere.
  • Parameters are the inputs; return hands a value back to the caller.
  • Functions that only do something (not return) use the void type.
  • Follow the DRY rule — "Don't Repeat Yourself." Repeated code wants to be a function.

Your Turn: Practice

Write a function called greet that takes a String name and returns the sentence "Welcome, [name]!". Then call it with three different names. Notice how you reused one function instead of writing the sentence three times.

Lesson 7 · Dart Language

Organising Data: Lists, Maps & Classes

Real apps rarely deal with one value at a time. A chat has many messages; a store has many products; a profile bundles a name, age and photo together. Dart gives us structures to organise groups of data cleanly. This lesson rounds out your Dart foundation.

Lists — Ordered Collections

A List holds many values in order. Each item has a position number called an index — and, as in almost every language, counting starts at 0, not 1.

List<String> names = ['Sara', 'Omar', 'Lina']; print(names[0]); // Sara (the first item) print(names[1]); // Omar (the second) print(names.length); // 3 (how many items) names.add('Karim'); // add a new item to the end

Lists power almost every scrolling screen you have ever used. The list of chats in WhatsApp, the feed in Instagram — all Lists of data being displayed one item at a time.

Maps — Labelled Data (Key & Value)

A Map stores values under named labels called keys, instead of numbered positions. It is perfect for describing one "thing" with several properties:

Map<String, dynamic> user = { 'name': 'Sara', 'age': 25, 'isPremium': true, }; print(user['name']); // Sara — look up a value by its key

Maps matter enormously because data arriving from the internet almost always comes in this key-and-value shape (a format called JSON, which you will meet in Course 4).

Classes — Designing Your Own Types

A Map works, but for important data we usually build a class: a custom blueprint that defines exactly what a "thing" looks like. This is the doorway to a huge topic called Object-Oriented Programming, and you only need the gentle basics for now.

// A blueprint describing what every User has class User { String name; int age; User(this.name, this.age); // how to build one } // Create actual users from the blueprint User sara = User('Sara', 25); print(sara.name); // Sara
Blueprint vs. building: A class is the architect's blueprint for a house — it describes what every house of this kind will have (rooms, doors). Each actual house built from it is called an object (or "instance"). One blueprint, many houses; one User class, many users.

Sets — Unique Collections

A Set is like a List, but it guarantees that every item is unique. If you try to add a duplicate, it simply ignores it. Sets use curly braces:

Set<String> tags = {'flutter', 'dart'}; tags.add('flutter'); // Ignored, already exists print(tags.length); // 2

Collection Superpowers: map and where

Dart collections have built-in methods that make manipulating data incredibly easy without writing manual loops:

List<int> numbers = [1, 2, 3, 4, 5]; // Get only even numbers var evens = numbers.where((n) => n % 2 == 0); // Multiply each number by 10 var tens = numbers.map((n) => n * 10);

The core is yours — now let's go deeper

You understand variables and types, operators, decisions, loops, functions, and how to organise data with Lists, Maps and Classes. That is already the complete core of a real programming language. In the next two lessons we push from "beginner" toward "professional" with two ideas every serious developer uses daily: Object-Oriented Programming and safe error handling.

Deep Dive: Advanced Collection Operations

Dart collections have powerful methods like fold and reduce to combine all elements into a single value. For example, summing up a list of numbers:

List<int> prices = [10, 20, 30]; // reduce takes the first two elements, applies the function, then takes the result and the next element int total = prices.reduce((value, element) => value + element); print(total); // 60

You can also quickly generate lists using List.generate:

List<String> items = List.generate(3, (index) => 'Item \$index'); print(items); // [Item 0, Item 1, Item 2]

Deep Dive: Spread Operator and Collection If/For

Dart provides incredibly elegant ways to build collections dynamically. The spread operator (...) unpacks a list into another list. Collection if and Collection for let you use logic directly inside a list declaration!

bool isPro = true; List<String> baseItems = ['Home', 'Settings']; List<String> menu = [ ...baseItems, // Spreads the items from baseItems if (isPro) 'Dashboard', // Collection if: adds only if condition is true for (var i = 1; i <= 2; i++) 'Item \$i' // Collection for: generates items inline ]; print(menu); // [Home, Settings, Dashboard, Item 1, Item 2]

You will use these features heavily in Flutter to dynamically construct the Widget tree (the UI) based on your app's state.

Deep Dive: Equatable and Object Equality

In Dart, two objects are not equal even if their contents are identical, because they live in different memory addresses. User('Alex') == User('Alex') is false. When managing lists of data or app state (like in Bloc or Riverpod), you must override the == operator and hashCode so Dart knows how to compare them.

class User { String name; User(this.name); @override bool operator ==(Object other) => identical(this, other) || other is User && runtimeType == other.runtimeType && name == other.name; @override int get hashCode => name.hashCode; }

Professionals usually use the Equatable package to do this automatically instead of writing it by hand!

Animated Example

Lists hold order; Maps hold labels

'Sara'
'Omar'
'Lina'
names[0] → 'Sara' · index starts at 0
Map username: 'Sara', age: 25
  1. A List holds many values in order: ['Sara', 'Omar', 'Lina'].
  2. Each has an index starting at 0 — so names[0] is 'Sara'.
  3. A Map stores values under named keys instead — perfect for describing one thing.

Key Takeaways

  • A List holds many ordered items, accessed by an index that starts at 0.
  • A Map stores values under named keys — the shape most internet data arrives in.
  • A class is a blueprint you design; each object built from it is an instance.
  • Real apps often use a list of objects — e.g. a list of users.

Your Turn: Practice

Create a List of five of your favourite foods, then write a for-in loop that prints each one with its position, like "1. Pizza". (Remember the index starts at 0, so you may add 1 for a human-friendly number.)

Lesson 8 · Dart Language

Object-Oriented Programming (OOP)

We briefly met classes in the last lesson. Now we go properly deep, because Object-Oriented Programming — OOP for short — is the way nearly all professional apps are structured. The idea is elegant: instead of scattering data and logic everywhere, you model the real world as objects, each bundling its own data and its own actions together.

Think of a real dog. A dog has things: a name, an age, a colour (these are its data). A dog also does things: it can bark, eat, sleep (these are its actions). OOP lets you build a Dog in code the same way — one neat package holding both what it is and what it does.

Properties and Methods

Inside a class, the data is stored in variables called properties, and the actions are written as functions called methods. Here is a complete, well-formed class:

class Dog { // Properties: the data every dog has String name; int age; // Constructor: how you build a new Dog Dog(this.name, this.age); // Method: an action a dog can perform void bark() { print('\$name says: Woof!'); } }

A class bundles data (properties) and actions (methods) together.

The Constructor: Building an Object

The special line Dog(this.name, this.age); is the constructor. It is the recipe for creating a new dog: it says "to make a Dog, give me a name and an age, and store them." Once the class (the blueprint) exists, you create real objects from it:

// Create two separate Dog objects from the one blueprint Dog rex = Dog('Rex', 3); Dog bella = Dog('Bella', 5); rex.bark(); // Rex says: Woof! bella.bark(); // Bella says: Woof! print(bella.age); // 5 — read a property with a dot

Notice how rex and bella are independent objects, each with its own name and age, yet both built from the same Dog class. This is the whole point: one blueprint, unlimited objects.

Inheritance: Building on What Exists

Here is where OOP becomes truly powerful. Suppose you need a Puppy that is just like a Dog but with an extra action. Rather than rewriting everything, a Puppy can inherit from Dog using the extends keyword — it automatically gains all of Dog's properties and methods, and can add its own:

class Puppy extends Dog { Puppy(String name, int age) : super(name, age); // A Puppy keeps bark(), and adds a new action void play() { print('\$name is playing happily!'); } }
Inheritance in one line: A Puppy "is a" Dog — so it should not repeat the Dog code, it should extend it. This mirrors real life: a smartphone is a phone with extra features, not a whole new invention.

Abstract Classes — Setting the Rules

Sometimes you want to create a blueprint that is so generic it shouldn't be built directly. An abstract class defines the rules (methods and properties) that other classes must follow, but leaves the specific implementation to them:

abstract class Animal { void makeSound(); // No body! Just a rule. } class Cat extends Animal { @override void makeSound() { print('Meow!'); // The Cat provides the implementation } }

Mixins — Adding Abilities

Inheritance lets you build on one class, but what if you want to add an ability to completely unrelated classes? Mixins let you "mix in" chunks of code wherever needed using the with keyword:

mixin Swimmer { void swim() => print('Swimming...'); } class Duck extends Animal with Swimmer { @override void makeSound() => print('Quack!'); } // Now a Duck can both makeSound() and swim()!

Why professionals rely on OOP

OOP keeps large apps organised and understandable. Each class is a self-contained unit you can reason about on its own. In Flutter, this matters enormously: every widget is a class, and when you build your own screens you will create your own classes that extend Flutter's. So OOP is not an abstract theory — it is literally how every Flutter app you will ever write is structured.

Deep Dive: Polymorphism & Interfaces

Polymorphism allows objects of different classes to be treated as objects of a common superclass. This is especially powerful with interfaces. In Dart, every class implicitly defines an interface. You use the implements keyword to force a class to follow a specific contract, meaning it MUST provide its own version of all the methods and properties.

class Database { void save(String data) {} } class LocalDatabase implements Database { @override void save(String data) { print('Saving \$data locally...'); } } class CloudDatabase implements Database { @override void save(String data) { print('Uploading \$data to cloud...'); } }

Deep Dive: Static Members & Factory Constructors

Sometimes a property or method belongs to the class itself, not to any specific object. We use the static keyword for this:

class MathHelper { static const double pi = 3.14159; static int add(int a, int b) => a + b; } print(MathHelper.pi); // Accessed directly on the class, no object needed

A factory constructor gives you more control when creating objects. Unlike a normal constructor which always makes a new instance, a factory can return an existing instance from a cache, or a subclass.

class Logger { static final Logger _cache = Logger._internal(); factory Logger() { return _cache; // Always returns the exact same object (Singleton pattern) } Logger._internal(); // Private named constructor }

Deep Dive: Enhanced Enums

An enum (enumeration) is a special type used to define a collection of constant values (like Days of the week or Loading states). Since Dart 2.17, enums have "superpowers" and can have their own properties, methods, and constructors, much like a class!

enum AppTheme { light('Light Mode', Colors.white), dark('Dark Mode', Colors.black); final String title; final Color color; // Const constructor for the enum const AppTheme(this.title, this.color); } void main() { AppTheme current = AppTheme.dark; print(current.title); // Dark Mode }

Deep Dive: Mixin Constraints (on)

Mixins are amazing, but sometimes a mixin requires the class it's applied to to have certain properties. You can restrict a mixin using the on keyword. This ensures the mixin can only be used on classes that extend a specific base class, allowing the mixin to safely call methods from that base class.

abstract class Widget { String name; Widget(this.name); } // This mixin can ONLY be used on classes that extend Widget mixin Draggable on Widget { void drag() => print('Dragging \$name'); // It knows "name" exists! } class Card extends Widget with Draggable { Card(String name) : super(name); }
Animated Example

One blueprint, many objects

class Dog
name, age
bark()
🐕 Rex · age 3
🐕 Bella · age 5
  1. First we design the blueprint: a Dog class with name, age and bark().
  2. From it we build a real object: Rex, age 3.
  3. And another: Bella, age 5. One class — unlimited independent objects.

Key Takeaways

  • Object-Oriented Programming models real things as objects with properties (data) and methods (actions).
  • A constructor is the special function that builds a new object from the class blueprint.
  • Methods are functions that live inside a class and act on its data.
  • Inheritance (extends) lets one class reuse and build upon another.

Your Turn: Practice

Design a Car class with two properties (brand and speed) and one method called accelerate() that increases the speed by 10 and prints the new speed. Then, on paper, create two Car objects and imagine calling accelerate() on each.

Lesson 9 · Dart Language

Writing Code That Doesn't Crash

This final Dart lesson is what separates code that works on your machine from code that survives real users doing unexpected things. Professionals do not just make apps work — they make apps that fail gracefully. Two Dart features make this possible: null safety and error handling.

The Billion-Dollar Problem: null

null means "no value at all — empty." It sounds harmless, but historically it has been the single biggest source of app crashes in the world. Imagine your code expects a user's name, but the name is null (perhaps they never entered it). Trying to use that empty value crashes the app.

Dart solves this with null safety: by default, a variable is not allowed to be null, so the mistake is caught while you write, not after you ship. If a value genuinely can be empty, you must say so explicitly with a ?:

String name = 'Sara'; // can NEVER be null — safe String? nickname = null; // the ? means "might be null"

Handling Possible Nulls Safely

When a value might be null, Dart gives you clean tools to handle it:

String? nickname = null; // Instead of crashing, fall back to a default: String displayName = nickname ?? 'Guest'; print(displayName); // Guest

The ?? operator turns a potential crash into a sensible default.

Catching Errors with try / catch

Some operations can fail for reasons outside your control: the internet drops, a file is missing, or a user types letters where a number was expected. Rather than letting the app crash, you wrap the risky code in a try block and handle any failure in a catch block:

try { // Risky: this fails if the text isn't a number int number = int.parse('hello'); print(number); } catch (error) { // Runs ONLY if something went wrong print('That was not a valid number. Please try again.'); } finally { // Runs no matter what — success or failure print('Done checking.'); }

Read the flow: Dart tries the risky line; if it fails, it jumps to catch and shows a friendly message instead of crashing; and finally always runs at the end for cleanup. Your user sees a calm "please try again," never a frozen or crashed screen.

Throwing Your Own Errors

You are not limited to catching system errors. You can (and should!) throw your own errors when someone tries to use your code incorrectly, using the throw keyword. This is called "failing fast":

void withdrawMoney(int amount) { if (amount <= 0) { throw Exception('Amount must be greater than zero!'); } // ... proceed with withdrawal }

Async Error Handling

When fetching data from the internet (which takes time), we use asynchronous programming. try / catch works perfectly there too, often combined with async and await:

Future<void> fetchData() async { try { var data = await api.getUser(); } catch (e) { print('Failed to load: \$e'); } }
The professional mindset: Beginners assume everything will go right. Professionals assume things will go wrong — bad input, lost connections, missing data — and plan for it. Every place your app touches the outside world (the internet, files, user typing) deserves error handling. This single habit is what makes an app feel reliable and trustworthy.

You've completed Dart — genuinely well

You now command the full Dart language: variables, logic, loops, functions, collections, object-oriented programming, null safety and error handling. This is not a watered-down "beginner subset" — it is the real toolkit professional Flutter developers use every day. With this foundation solid, you are ready to build beautiful, robust interfaces in Course 3.

Deep Dive: Custom Exceptions

For large apps, standard errors aren't descriptive enough. You can create your own exception classes by implementing the Exception interface. This makes your error handling much more specific and readable.

class InsufficientFundsException implements Exception { final String message; InsufficientFundsException(this.message); @override String toString() => 'InsufficientFundsException: \$message'; } void transferMoney(double balance, double amount) { if (amount > balance) { throw InsufficientFundsException('Cannot transfer \$amount, balance is only \$balance.'); } }

Deep Dive: Global Error Catching in Flutter

Even with perfect try/catch blocks, a bug might slip through. If an error is completely unhandled, a Flutter app normally shows a scary red screen of death. Professionals intercept these using FlutterError.onError and PlatformDispatcher to log the crash to services like Firebase Crashlytics and show a polite fallback screen instead.

void main() { // Catch framework-level errors (like layout issues) FlutterError.onError = (details) { logCrashToServer(details.exception, details.stack); }; // Catch asynchronous Dart errors (like failed API calls) PlatformDispatcher.instance.onError = (error, stack) { logCrashToServer(error, stack); return true; // Prevents the app from crashing entirely }; runApp(MyApp()); }
Animated Example

try / catch turns a crash into a message

int.parse('hello')
✗ crash (unhandled)
✓ caught → friendly message
  1. A risky line — parsing "hello" as a number — fails. Unhandled, it would crash the whole app.
  2. Wrapped in try/catch, the failure is caught and you show a calm "please try again" instead. Professionals plan for what goes wrong.

Key Takeaways

  • null means "no value at all" — and Dart's null safety forces you to handle it deliberately.
  • A ? after a type (String?) marks a variable that is allowed to be null.
  • ?? provides a fallback, and ?. safely accesses something that might be null.
  • Wrap risky code in try / catch so a failure shows a friendly message instead of crashing.

Your Turn: Practice

Write a small try / catch block that attempts to convert the string "hello" into a number with int.parse('hello') (which will fail). In the catch, print "That wasn't a valid number." This is exactly how real apps handle bad input gracefully.

Lesson 10 · Dart Language

Asynchronous Programming

In the real world, many tasks take time: downloading an image, fetching data from a server, or reading a large file. If our app waits completely frozen for these tasks to finish, it feels broken. Asynchronous programming lets our code say, "Start this slow task, let me keep doing other things (like responding to user taps), and tell me when it's done."

Futures: A Promise of Data

In Dart, a Future represents a value that isn't ready yet, but will be in the future. Think of it like a restaurant receipt: you don't have your food immediately, but you hold a promise that you'll get it soon.

Future<String> fetchUserOrder() { // Imagine this takes 2 seconds over the internet return Future.delayed(Duration(seconds: 2), () => 'Cappuccino'); }

async and await

The cleanest way to handle a Future is using the async and await keywords. They allow you to write asynchronous code that looks just like normal, line-by-line code.

void serveCustomer() async { print('Ordering...'); // The 'await' pauses THIS function until the Future completes, // but the rest of the app keeps running! String order = await fetchUserOrder(); print('Here is your $order!'); }

Streams: A Flow of Data

While a Future gives you exactly one value eventually, a Stream gives you a sequence of values over time. Think of it like a hose pipe of data. We use streams for things like location updates, chatting in real-time, or a countdown timer.

Stream<int> countDown() async* { for (int i = 3; i > 0; i--) { await Future.delayed(Duration(seconds: 1)); yield i; // Sends a value out into the stream } } void startTimer() async { await for (int number in countDown()) { print(number); // Prints 3, then 2, then 1, one second apart } print('Go!'); }

Deep Dive: StreamController

While async* is great for creating streams, sometimes you need to manually add events to a stream from different parts of your app (like when a user clicks a button). For this, we use a StreamController.

import 'dart:async'; void main() { // Create a controller that controls a Stream of Strings var controller = StreamController<String>(); // Listen to the stream (the sink's output) controller.stream.listen((event) { print('Received: \$event'); }); // Add data to the stream via the sink controller.sink.add('User logged in'); controller.sink.add('Message received'); // Always close controllers when done to prevent memory leaks controller.close(); }

Deep Dive: Executing Futures in Parallel

What if you need to fetch the User's profile, their friends list, and their unread messages all at once? If you await them one by one, it takes a long time. Future.wait() allows you to run multiple asynchronous tasks simultaneously and wait for them all to finish together.

Future<void> loadDashboard() async { // Starts all three requests at the exact same time var results = await Future.wait([ fetchProfile(), fetchFriends(), fetchMessages() ]); print('All data loaded in parallel!'); }

Deep Dive: The Completer

Sometimes you need to create a Future from scratch based on some old callback-based API, or you want to manually trigger when a Future is finished from completely different parts of your code. A Completer gives you the controls to a Future: you can return its future immediately, and later call complete() when you're ready.

import 'dart:async'; class AudioPlayer { final _completer = Completer<bool>(); Future<bool> play() { // Start hardware audio engine... // Return the future immediately, even though it's not done return _completer.future; } // Called by some hardware callback event later void _onHardwareFinished() { _completer.complete(true); // Resolves the future! } }

Key Takeaways

  • Future represents a single value that will be available later.
  • async and await make dealing with Futures clean and readable.
  • Stream delivers multiple values over time, perfect for real-time events.

Your Turn: Practice

Write a function marked with async that uses await Future.delayed(...) to simulate loading data for 3 seconds, then prints a success message.

Lesson 11 · Dart Language

Advanced Dart Features

To truly master Dart and write code like a professional, there are a few advanced tools that make your code cleaner, safer, and incredibly powerful. These features reduce repetition and allow for highly flexible structures.

Extensions: Adding Superpowers

What if you want a new method on String or int, but you didn't write those classes? Extensions let you add new methods to existing classes without touching their original code.

extension StringSuperpowers on String { // Adds a custom 'capitalize' method to every String String capitalize() { if (this.isEmpty) return this; return '${this[0].toUpperCase()}${this.substring(1)}'; } } void main() { String name = 'sara'; print(name.capitalize()); // Sara }

Generics: Flexible Types

Sometimes you want a class or function to work with any type, while still keeping Dart's type safety. This is what Generics (the <T> syntax) are for. You've already used them with List<String>!

// A box that can hold ANY type securely class Box<T> { T content; Box(this.content); } void main() { var stringBox = Box<String>('Secret Document'); var numberBox = Box<int>(42); }

Isolates: Heavy Lifting

Even with async, Dart runs on a single thread. If you do something incredibly heavy (like processing a huge image or complex encryption), the app will stutter. Isolates allow you to run heavy tasks on a completely separate processor core.

import 'dart:isolate'; // A heavy function running in a separate Isolate void heavyTask(SendPort port) { int total = 0; for (int i = 0; i < 1000000000; i++) { total += i; } port.send(total); } void main() async { var receivePort = ReceivePort(); await Isolate.spawn(heavyTask, receivePort.sendPort); int result = await receivePort.first; print('Result: \$result'); }

Dart 3 Magic: Records and Pattern Matching

With Dart 3, you can return multiple values from a function cleanly using Records, and use Pattern Matching to easily destructure and check data shapes. This completely modernizes how you write Dart.

// Returning multiple values with a Record (String, int) getUserInfo() { return ('Alex', 28); } void main() { // Pattern Matching: destructuring the record var (name, age) = getUserInfo(); print('\$name is \$age years old'); // Switch expressions with pattern matching var shape = ('circle', 10); String description = switch (shape) { ('circle', int radius) => 'Circle with radius \$radius', ('square', int size) => 'Square with size \$size', _ => 'Unknown shape', }; print(description); }

Deep Dive: Sealed Classes (Dart 3)

Dart 3 introduced class modifiers, the most prominent being the sealed class. A sealed class allows you to define a strictly closed set of subtypes. The compiler knows all possible subclasses, meaning a switch statement over a sealed class can be exhaustively checked (if you miss a case, it throws an error!).

sealed class AuthState {} class Unauthenticated extends AuthState {} class Loading extends AuthState {} class Authenticated extends AuthState { final String user; Authenticated(this.user); } Widget buildUI(AuthState state) { // The compiler GUARANTEES we handled every possible state! return switch (state) { Unauthenticated() => LoginScreen(), Loading() => LoadingSpinner(), Authenticated(user: var u) => HomeScreen(user: u), }; }

This pattern is arguably the most robust way to manage UI state in modern Flutter applications.

Deep Dive: Extension Types (Dart 3.3)

Dart 3.3 introduced Extension Types. They look like wrappers around existing types (like an `int` or a `String`), adding specific methods to them, but they exist only at compile-time. This means zero performance cost or memory overhead at runtime! It's perfect for things like typed IDs.

// Wrapping an int so we don't mix up Product IDs and User IDs extension type UserId(int value) {} void fetchUser(UserId id) { print('Fetching user \${id.value}'); } void main() { UserId myId = UserId(42); fetchUser(myId); // fetchUser(42); // COMPILE ERROR: Requires UserId, not int! }

At runtime, Dart erases `UserId` completely and treats it just like a raw `int`, making it incredibly fast.

Key Takeaways

  • Extensions add new methods to existing types.
  • Generics <T> allow writing flexible code that works safely with multiple types.
  • Isolates are used for true parallel multi-threading for heavy processing.
Lesson 12 · Advanced Architecture

Design Patterns in Dart

As you build larger apps, you'll encounter the same structural problems repeatedly. Design Patterns are proven, standardized solutions to these common problems. They make your code more modular, testable, and easier for other developers to understand.

The Singleton Pattern

Sometimes you want a class to have exactly one instance throughout the entire lifecycle of your app (like a Database connection or an Authentication service). The Singleton pattern guarantees this.

class DatabaseService { // The single internal instance static final DatabaseService _instance = DatabaseService._internal(); // The factory constructor returns the same instance every time factory DatabaseService() { return _instance; } // Private constructor DatabaseService._internal(); void connect() => print('Connected to DB!'); } void main() { var db1 = DatabaseService(); var db2 = DatabaseService(); print(identical(db1, db2)); // true - they are the exact same object in memory! }

The Factory Pattern

When you have a superclass and multiple subclasses, and you want to delegate the responsibility of which subclass to instantiate to a special method, you use the Factory pattern.

abstract class Notification { void send(String message); // Factory method factory Notification(String type) { switch (type) { case 'email': return EmailNotification(); case 'push': return PushNotification(); default: throw Exception('Unknown type'); } } } class EmailNotification implements Notification { void send(String message) => print('Email sent: \$message'); } class PushNotification implements Notification { void send(String message) => print('Push sent: \$message'); } void main() { var notif = Notification('push'); notif.send('Hello!'); // Push sent: Hello! }

The Observer Pattern (Streams)

The Observer pattern is used when you want an object to notify other objects about changes in its state. In Dart, this is elegantly built-in using Streams and StreamController. It's the foundation of Reactive Programming.

import 'dart:async'; class WeatherStation { final _controller = StreamController<double>.broadcast(); Stream<double> get temperatureStream => _controller.stream; void updateTemperature(double temp) { _controller.sink.add(temp); // Notifies all observers! } } void main() { var station = WeatherStation(); // Observer 1 (Phone App) station.temperatureStream.listen((temp) => print('Phone UI updated: \$temp°')); // Observer 2 (Website) station.temperatureStream.listen((temp) => print('Web UI updated: \$temp°')); station.updateTemperature(25.5); // Both observers react instantly }

The Repository Pattern

As your app grows, you might fetch data from a local database, an API, or a cache. The UI shouldn't care where the data comes from. The Repository Pattern creates a clean boundary: the UI asks the Repository for data, and the Repository decides whether to fetch it from the API or local storage.

class UserRepository { final LocalDatabase local; final NetworkApi network; UserRepository(this.local, this.network); Future<User> getUser(String id) async { // 1. Check cache first for speed User? cachedUser = await local.getUser(id); if (cachedUser != null) return cachedUser; // 2. If not in cache, fetch from internet User freshUser = await network.fetchUser(id); await local.saveUser(freshUser); // Save for next time return freshUser; } }

This pattern is essential for offline-first applications and makes your code incredibly easy to test, as you can easily mock the database or API.

Key Takeaways

  • Singleton ensures only one instance of a class exists.
  • Factory abstracts away object creation logic.
  • Observer (Streams) enables reactive programming where multiple parts of the app listen to state changes.
Lesson 13 · Advanced Architecture

Memory Management & Performance

Professional developers don't just write code that works; they write code that is efficient. Dart uses a Garbage Collector to manage memory, but poorly written code can still cause memory leaks and stuttering interfaces.

Understanding the Garbage Collector

Dart periodically scans your app's memory to find objects that are no longer being used (objects that have no references pointing to them). When it finds them, it automatically deletes them to free up RAM. This is called Garbage Collection (GC).

However, if you accidentally keep a reference to an object, the GC cannot delete it. This is a Memory Leak.

Common Memory Leak: Forgetting to Dispose

The most common cause of memory leaks in Dart and Flutter is forgetting to close Streams, Timers, or Controllers. If a Stream keeps listening forever, the object holding it can never be garbage collected.

class BadComponent { late StreamSubscription _sub; void init() { // Subscribing, but never unsubscribing! _sub = globalStream.listen((data) => print(data)); } } class GoodComponent { late StreamSubscription _sub; void init() { _sub = globalStream.listen((data) => print(data)); } // ALWAYS provide a way to clean up resources! void dispose() { _sub.cancel(); // Stops listening, allowing GC to clean up } }

Constant Objects (const)

Every time you create an object, it takes up memory. If an object's properties never change, you can declare its constructor as const. Dart will create it exactly once in memory and reuse that exact same instance everywhere in your app, drastically improving performance.

class PaddingSettings { final double value; // Const constructor! const PaddingSettings(this.value); } void main() { // These point to the EXACT SAME memory address var p1 = const PaddingSettings(16.0); var p2 = const PaddingSettings(16.0); print(identical(p1, p2)); // true! Zero extra memory used. }

Deep Dive: Isolates vs Async

Remember that async/await does NOT run code in parallel. It just pauses a function to let the single main thread do other things. If you do heavy math inside an async function, the main thread will still freeze, causing your app's animations to stutter (UI jank).

For heavy computation (like parsing a massive JSON file or applying an image filter), you MUST use Isolate.run() (introduced in Dart 2.19). It moves the work to a background CPU thread entirely.

import 'dart:isolate'; // A heavy, CPU-intensive task int slowFibonacci(int n) { if (n <= 1) return n; return slowFibonacci(n - 1) + slowFibonacci(n - 2); } void main() async { print('Calculating...'); // Isolate.run spawns a background thread, runs the function, // returns the result, and cleans itself up automatically! int result = await Isolate.run(() => slowFibonacci(40)); print('Result: \$result'); // The UI never froze! }

Key Takeaways

  • Always cancel() Subscriptions and close() Controllers to prevent memory leaks.
  • Use const constructors for objects that never change to save RAM.
  • Use Isolate.run() for heavy CPU tasks to prevent the UI from freezing.