Volume 1: Dart Foundations
Volume 1: Dart Foundations
Learning Objectives
- Understand why Dart exists and how it executes code.
- Master variables, types, and the critical concept of Null Safety.
- Understand Object-Oriented Programming (OOP) in Dart.
- Grasp asynchronous programming (Futures and Streams) for network requests.
Why Does This Exist?
You cannot master Flutter without mastering Dart. Flutter is merely a framework written in Dart. When you create a button, you are instantiating a Dart class. When you fetch data from an API, you are using Dart's asynchronous Futures. If you try to learn Flutter by copy-pasting code without understanding Dart, you will hit an impenetrable wall within a week.
The Problem Before the Solution
Before Dart 2.12, developers lived in fear of the "Billion Dollar Mistake": the Null Pointer Exception. You would fetch a user's profile from a database, try to print their name, and the entire app would crash because the user didn't have a name, and you didn't check for it.
Mental Model: Null Safety
Imagine a box that is supposed to contain a cat. In old programming languages, the box could contain a cat, or it could be completely empty (null). If you tell the box to "meow", and the box is empty, the universe explodes (your app crashes).
Dart's Sound Null Safety changes the physics of the universe. If a box is labeled "Cat", the compiler guarantees there is a cat inside. It is physically impossible for the box to be empty. If you want a box that might be empty, you must label it "Cat?". When you try to make a "Cat?" meow, the compiler will stop you and say, "Hey, you need to check if there's actually a cat in there first!"
Syntax: Variables and Null Safety
// 1. A guaranteed String. Cannot be null.
String name = 'Tarun';
// 2. A String that might be null (notice the ?)
String? nickname = null;
// 3. Type inference (Dart knows this is an int)
var age = 25;
// 4. Immutability (Cannot be changed once set)
final String city = 'New York';
const double pi = 3.14159;🔍 Checkpoint: What is the difference between final and const?
const is a compile-time constant. Its value must be known before the app even runs (e.g., const double pi = 3.14). final is a runtime constant. It can be set once when the app is running (e.g., fetching the time when the user clicks a button: final time = DateTime.now()), but it can never be changed after that.
Functions and Parameters
Dart makes functions incredibly readable through Named Parameters. When a function has many arguments, remembering the order is a nightmare. Named parameters solve this.
// BAD EXAMPLE: What do these booleans mean?
createUser('Tarun', true, false, 25);
// GOOD EXAMPLE: Named parameters
void createUser({
required String name,
required int age,
bool isAdmin = false, // Default value
}) {
print('User $name created.');
}
// Calling the function:
createUser(name: 'Tarun', age: 25, isAdmin: true);Classes and OOP
Dart is an Object-Oriented language. Everything you see on a screen in Flutter is an Object created from a Class.
class User {
// Properties
final String name;
final int age;
// Constructor with named, required parameters
User({required this.name, required this.age});
// Method
void introduce() {
print('Hi, I am $name');
}
}
// Instantiation
final user = User(name: 'Tarun', age: 25);
user.introduce();Mental Model: Asynchronous Programming
Imagine you are cooking dinner. You put pasta in boiling water. Do you stand frozen, staring at the pot for 10 minutes, unable to move or breathe? No. You start chopping vegetables while the pasta boils. When the pasta is done, a timer rings, and you return to it.
This is asynchronous programming. When your app asks a server for data over the internet, it takes time. If your code waits synchronously, the entire app freezes. The user can't scroll, buttons don't click, and the OS will eventually show an "App Not Responding" dialog.
Dart solves this using Future and the async / await keywords.
// A function that simulates a slow network request
Future<String> fetchUserData() async {
// Await tells Dart to go do other things (keep the UI smooth)
// while we wait for 2 seconds.
await Future.delayed(Duration(seconds: 2));
return 'Tarun Data';
}
void loadData() async {
print('1. Requesting data...');
// We MUST await the Future to unpack the actual String
final data = await fetchUserData();
print('2. Data received: $data');
}Break It & Debug It
The Bug: You write code to fetch a user, but instead of the user's name showing on the screen, the screen literally displays the text Instance of 'Future<String>'.
The Reason: You forgot the await keyword! You didn't wait for the pasta to finish boiling; you literally tried to serve the boiling pot of water to the customer.
🔧 Mini Project ⏱ 20 min
Goal: The Type-Safe Contact Book
Build a contact book in pure Dart using null safety. A Contact class with required name (String) and optional phone (String?). A ContactBook class with methods: add(Contact), findByName(String) returns Contact?, deleteByName(String) returns bool. Use null safety properly throughout.
💡 See One Approach (Mini Project)
One valid solution — yours may differ.
class Contact {
final String name;
final String? phone; // nullable — might not have phone
final String? email;
const Contact({required this.name, this.phone, this.email});
@override
String toString() => 'Contact(name: $name, phone: ${phone ?? "N/A"}, email: ${email ?? "N/A"})';
}
class ContactBook {
final List<Contact> _contacts = [];
void add(Contact contact) => _contacts.add(contact);
Contact? findByName(String name) {
try {
return _contacts.firstWhere(
(c) => c.name.toLowerCase() == name.toLowerCase(),
);
} on StateError {
return null; // Not found — return null safely
}
}
bool deleteByName(String name) {
final contact = findByName(name);
if (contact == null) return false;
_contacts.remove(contact);
return true;
}
void listAll() {
if (_contacts.isEmpty) {
print('Contact book is empty.');
return;
}
for (final c in _contacts) print(' • $c');
}
}
void main() {
final book = ContactBook();
book.add(Contact(name: 'Tarun', phone: '+91 98765 43210'));
book.add(Contact(name: 'Alice', email: 'alice@example.com'));
book.add(Contact(name: 'Bob', phone: '+1 555 0100', email: 'bob@example.com'));
print('=== All Contacts ===');
book.listAll();
print('\n=== Search ===');
final found = book.findByName('alice');
print(found != null ? 'Found: $found' : 'Not found');
print('\n=== Delete ===');
print('Deleted Bob: ${book.deleteByName("Bob")}');
print('Deleted Eve: ${book.deleteByName("Eve")}'); // false
book.listAll();
}
🏗 Bigger Project ⏱ 1.5 hrs
Goal: Async News Feed
Build a simulated news feed fetcher in pure Dart. A NewsArticle class with title, body, publishedAt (DateTime), and author (nullable). A NewsFeedService with: fetchLatest() returns Future<List<NewsArticle>> (simulate network delay with Future.delayed), fetchStream() returns Stream<NewsArticle> that emits one article every 500ms (simulate live feed). Handle errors with try/catch. A main() that listens to the stream, displays each article, and times out after 5 articles.
💡 See One Approach (Bigger Project)
One valid solution — yours may differ.
import 'dart:async';
class NewsArticle {
final String title;
final String body;
final DateTime publishedAt;
final String? author; // nullable
NewsArticle({
required this.title,
required this.body,
required this.publishedAt,
this.author,
});
@override
String toString() =>
'[${publishedAt.hour}:${publishedAt.minute.toString().padLeft(2,'0')}] '
'${title} — ${author ?? "Anonymous"}';
}
class NewsFeedService {
final List<NewsArticle> _fakeData = [
NewsArticle(title: 'Flutter 4.0 Released!', body: 'Major rendering overhaul...', publishedAt: DateTime.now(), author: 'Tech Editor'),
NewsArticle(title: 'Dart Null Safety Saves Lives', body: 'Billions of NPEs prevented...', publishedAt: DateTime.now()),
NewsArticle(title: 'Riverpod 3.0 Ships', body: 'Breaking changes incoming...', publishedAt: DateTime.now(), author: 'Remi Rousselet'),
NewsArticle(title: 'iOS 20 Announced', body: 'New APIs for Flutter devs...', publishedAt: DateTime.now()),
NewsArticle(title: 'Android 16 Preview', body: 'Predictive back gesture...', publishedAt: DateTime.now(), author: 'Android Team'),
NewsArticle(title: 'App Store Rejects 1M Apps', body: 'Quality crackdown...', publishedAt: DateTime.now()),
];
// Future: fetch all at once (simulates REST API)
Future<List<NewsArticle>> fetchLatest() async {
await Future.delayed(const Duration(milliseconds: 300)); // network latency
return _fakeData;
}
// Stream: emit one article at a time (simulates WebSocket/SSE)
Stream<NewsArticle> fetchStream() async* {
for (final article in _fakeData) {
await Future.delayed(const Duration(milliseconds: 500));
yield article; // yield sends the value to the stream listener
}
}
}
void main() async {
final service = NewsFeedService();
// 1. Batch fetch with Future
print('=== Fetching latest (Future) ===');
try {
final articles = await service.fetchLatest();
print('Fetched ${articles.length} articles.');
} catch (e) {
print('Error: $e');
}
// 2. Live stream — stop after 3 articles
print('\n=== Live Stream (first 3 articles) ===');
int count = 0;
await for (final article in service.fetchStream()) {
print(' $article');
count++;
if (count >= 3) break; // Stop listening after 3
}
print('Stream stopped after $count articles.');
}
🎯 Interview Questions
Answer these before revealing. These appear in real Flutter/Dart interviews.
🔍 Easy: What is the difference between `var`, `final`, and `const` in Dart?
`var` is mutable — the variable can be reassigned. `final` is a runtime constant — assigned once, cannot be reassigned after that, but the value can be determined at runtime (e.g., `final time = DateTime.now()`). `const` is a compile-time constant — the value must be known before the app runs (e.g., `const pi = 3.14`). `const` objects are canonicalized, meaning `const Text('hello') == const Text('hello')` is true in Flutter.
🔍 Medium: Explain Dart's `async`/`await` and how it differs from multi-threading.
Dart is single-threaded. `async`/`await` does NOT create new threads. Instead, it uses an event loop — when you `await` a Future, the current function is suspended and control returns to the event loop, which can process other events. When the awaited task completes (e.g., network response arrives), the function resumes. True multi-threading in Dart requires Isolates, which have separate memory and communicate via message passing.
🔍 Hard: What is Sound Null Safety and how does Dart's type system enforce it at compile time, not just runtime?
Sound Null Safety means the Dart analyzer can prove at compile time that no null dereference can occur — not just warn about it. This is achieved through flow analysis: the compiler tracks the type of a variable through if/else branches. After `if (x != null) { x.length }`, Dart knows x is non-null inside the block. This is "sound" because the guarantee holds throughout the entire program, including through generics and async code. Variables are non-nullable by default; you must explicitly opt into nullability with `?`. This lets Dart eliminate null checks in generated code, improving performance.