Module 01: Dart Essentials
Teach: How to declare variables, work with Dart's type system, handle null safely, and write functions. See: Real Dart code that demonstrates each concept with clear before/after examples. Feel: Confident that Dart is approachable and that its strictness is a superpower, not a burden.
Variables: var, final, and const
Teach: The three ways to declare variables in Dart and when to use each one.
See: How var, final, and const behave differently when you try to reassign them.
Feel: Clear-headed about which keyword to reach for in any situation.
Let's start with the most basic building block: putting values into boxes (variables).
Dart gives you three ways to declare a variable:
var name = 'Campbell'; // Mutable — can change later
final age = 22; // Set once at runtime — can't change
const pi = 3.14159; // Compile-time constant — baked in forever
Think of var as a whiteboard — you can erase and rewrite. final is a permanent marker — once you write, it's done. const is carved in stone — it was decided before the program even ran.
var — The Flexible One
var greeting = 'Hello';
greeting = 'Hi there'; // Totally fine
greeting = 42; // ERROR! Dart inferred greeting as String
Wait — why the error? Because Dart uses type inference. When you wrote var greeting = 'Hello', Dart quietly decided: "That's a String." From that point on, greeting is locked to String values. You can change what string it holds, but not what type it holds.
final — Set It and Forget It
final city = 'Portland';
city = 'Seattle'; // ERROR! final variables can't be reassigned
final timestamp = DateTime.now(); // Computed at runtime — that's fine
Use final when a value shouldn't change after it's assigned. In Flutter, you'll use final constantly — most widget properties are set once and never mutated.
const — The Immutable Hardliner
const gravity = 9.81; // Known at compile time — perfect
const now = DateTime.now(); // ERROR! Can't compute at compile time
const is stricter than final. The value must be knowable before the program runs. Use it for true constants: math values, configuration strings, fixed lists.
Default to final. Use const when the value is a true compile-time constant. Use var only when you genuinely need to reassign.
The Type System
Teach: How Dart's type system works, including core types, type inference, and generic collections. See: Explicit and inferred type declarations side by side, plus List, Map, and Set in action. Feel: Confident that strong typing is a helpful guardrail, not a burden.
Dart is strongly typed — every value has a type, and the compiler checks that you're using types correctly. But thanks to type inference, you rarely have to write the types yourself.
Core Types
int count = 42;
double price = 9.99;
String name = 'Campbell';
bool isAwesome = true;
You can also let Dart figure out the type:
var count = 42; // Dart infers int
var price = 9.99; // Dart infers double
var name = 'Campbell'; // Dart infers String
var isAwesome = true; // Dart infers bool
Both styles are equivalent. The explicit style is useful for documentation or when the type isn't obvious.
Collections: List, Map, Set
// List — ordered, allows duplicates (like an array)
var fruits = ['apple', 'banana', 'cherry'];
fruits.add('date');
print(fruits[0]); // apple
// Map — key-value pairs (like a dictionary or object)
var scores = {
'Campbell': 95,
'Alice': 88,
'Bob': 72,
};
print(scores['Campbell']); // 95
// Set — unordered, no duplicates
var tags = {'flutter', 'dart', 'mobile'};
tags.add('flutter'); // Ignored — already exists
print(tags.length); // 3
If you're coming from JavaScript: List is like an Array, Map is like an Object (or Map), and Set is like... well, a Set. Dart's collections are generic, meaning List<String> is a list that only holds strings. The compiler catches it if you try to sneak in an int.
Type Annotations on Collections
List<String> names = ['Campbell', 'Alice'];
Map<String, int> ages = {'Campbell': 22, 'Alice': 25};
Set<double> temperatures = {72.5, 68.3, 75.1};
The angle brackets (<String>, <String, int>) are generics — they tell Dart what types the collection holds.
Null Safety: Dart's Safety Net
Teach: How Dart's null safety system prevents the "billion dollar mistake" and how to work with nullable types. See: The difference between nullable and non-nullable types in action. Feel: Appreciation for how the compiler catches bugs before they happen.
In many languages, any variable can be null at any time. This leads to the dreaded NullPointerException — the single most common runtime crash in software history. Tony Hoare, who invented null references, called it his "billion dollar mistake."
Dart says: no more. By default, variables cannot be null.
String name = 'Campbell';
name = null; // ERROR! String can't be null
Making a Variable Nullable with ?
If you genuinely need a value that might be absent, you opt in with ?:
String? nickname = 'Cam';
nickname = null; // Fine! String? means "String or null"
The ? is part of the type. String and String? are different types. The compiler tracks which is which and forces you to handle the null case.
The Operators: !, ??, ??=, ?.
Dart gives you a toolbox of operators for working with nullable types:
The ! operator — "Trust me, it's not null"
String? maybeName = getName();
String definitelyName = maybeName!; // Crashes if null!
Use ! sparingly. It's an escape hatch that says "I know better than the compiler." If you're wrong, your app crashes.
The ?? operator — "Use this default if null"
String? maybeName = null;
String displayName = maybeName ?? 'Anonymous';
print(displayName); // Anonymous
This is the safe way to handle nulls. Much better than !.
The ??= operator — "Assign only if currently null"
String? label;
label ??= 'Default'; // label is now 'Default'
label ??= 'Other'; // label stays 'Default' (not null, so no reassign)
The ?. operator — "Call this method only if not null"
String? name = null;
print(name?.toUpperCase()); // null (no crash!)
print(name?.length); // null (no crash!)
String? name2 = 'Campbell';
print(name2?.toUpperCase()); // CAMPBELL
Null safety might feel annoying at first — "Why can't I just use null wherever?" But trust the process. Every time the compiler complains about a null, it's catching a bug you would have spent 30 minutes debugging at runtime. The compiler is your friend. A strict, unyielding friend who's always right.
Prefer ?? over !. The ?? operator provides a safe default. The ! operator is a gamble.
Functions
Teach: How to write functions in Dart — basic, arrow, named parameters, optional parameters, and first-class functions.
See: Functions passed as arguments, stored in variables, and used with higher-order methods like map and sort.
Feel: Empowered by how expressive and flexible Dart functions are.
Functions in Dart are flexible, expressive, and — here's the fun part — they're first-class citizens, meaning you can pass them around like any other value.
Basic Functions
int add(int a, int b) {
return a + b;
}
void greet(String name) {
print('Hello, $name!');
}
Arrow Syntax (for one-liners)
If your function body is a single expression, use the arrow:
int add(int a, int b) => a + b;
void greet(String name) => print('Hello, $name!');
Same behavior, less ceremony.
Named Parameters
Named parameters make function calls readable — especially when there are many arguments:
void createUser({
required String name,
required String email,
int age = 0,
String role = 'viewer',
}) {
print('$name ($email), age $age, role: $role');
}
// Calling it — order doesn't matter:
createUser(
email: 'campbell@example.com',
name: 'Campbell',
role: 'admin',
);
Notice:
- Named params are wrapped in {}
- required means the caller must provide it
- Without required, you must provide a default value
Flutter uses named parameters everywhere. When you see Text('Hello', style: TextStyle(fontSize: 24)), those are named parameters at work. Get comfortable with them now — they'll be your daily bread.
Optional Positional Parameters
String greet(String name, [String? title]) {
if (title != null) {
return 'Hello, $title $name!';
}
return 'Hello, $name!';
}
print(greet('Campbell')); // Hello, Campbell!
print(greet('Campbell', 'Mr.')); // Hello, Mr. Campbell!
Functions as First-Class Citizens
You can store functions in variables, pass them as arguments, and return them from other functions:
// Store a function in a variable
var multiply = (int a, int b) => a * b;
print(multiply(3, 4)); // 12
// Pass a function as an argument
var numbers = [3, 1, 4, 1, 5];
numbers.sort((a, b) => a.compareTo(b));
print(numbers); // [1, 1, 3, 4, 5]
// Use built-in higher-order functions
var doubled = numbers.map((n) => n * 2).toList();
print(doubled); // [2, 2, 6, 8, 10]
String Interpolation
Teach: How to embed variables and expressions inside strings using Dart's $ syntax.
See: Clean interpolation replacing ugly string concatenation.
Feel: Relief that building strings in Dart is simple and readable.
Dart makes string building easy with the $ operator:
var name = 'Campbell';
var age = 22;
// Simple variable — just $
print('My name is $name');
// Expression — use ${}
print('Next year I will be ${age + 1}');
print('Name in caps: ${name.toUpperCase()}');
print('Name length: ${name.length}');
Forget string concatenation with +. In Dart, interpolation with $ is cleaner, faster, and the standard way. You'll see it in every Flutter codebase. Use $variable for simple references and ${expression} when you need to compute something inside the string.
// Don't do this:
var message = 'Hello, ' + name + '! You are ' + age.toString() + ' years old.';
// Do this instead:
var message = 'Hello, $name! You are $age years old.';
There Are No Dumb Questions
Teach: Answers to common beginner questions about variables, types, strings, and collections. See: Concise explanations that clear up the most frequent points of confusion. Feel: Reassured that these are normal questions and the answers are simple.
Q: When should I use var vs writing the type explicitly?
A: Use var when the type is obvious from the right side (var name = 'Campbell' — clearly a String). Write the type explicitly when it adds clarity, especially for complex types or when the right side doesn't make the type obvious (List<Map<String, dynamic>> data = fetchData()).
Q: What's the difference between final and const again?
A: final is evaluated at runtime — you can assign final x = DateTime.now(). const is evaluated at compile time — the value must be fully determined before the program runs. In Flutter, you'll see const used with widget constructors: const Text('Hello') tells Flutter this widget never changes, enabling performance optimizations.
Q: Can I use single quotes and double quotes interchangeably?
A: Yes! Dart treats 'hello' and "hello" identically. The convention is to use single quotes, but either works. Use the other kind when your string contains quotes: "It's Flutter time!" or 'She said "wow"'.
Q: Why does Dart need both List and Set?
A: List preserves order and allows duplicates — use it when sequence matters. Set is unordered and automatically removes duplicates — use it when you need uniqueness. Checking "does this Set contain X?" is also much faster than checking a List.
These four questions come up in every intro-to-Dart class I've ever taught. If you're wondering about any of them, you're right on schedule. A quick tip on the var versus explicit-type question: in a Flutter codebase, you'll see both. Widget code tends to use var because the right-hand side is obvious. Data-layer code tends to spell out types because the shape of the data is the important part. Read other people's code and you'll pick up the conventions naturally.
Sharpen Your Pencil
Teach: How to apply variables, null safety, and functions through hands-on coding exercises. See: Working Dart programs that you type out, run, and experiment with. Feel: The muscle memory forming as syntax moves from your screen into your fingers.
Where this fits: These exercises build your Dart muscle memory. Every Flutter app is Dart code underneath, so these fundamentals are non-negotiable.
Exercise 1: variables.dart
Create a file called variables.dart and write code that:
- Declares a
var, afinal, and aconstvariable - Tries to reassign each one (comment out the lines that cause errors, and add a comment explaining why)
- Creates a
List<String>of your five favorite movies - Creates a
Map<String, int>of three cities and their populations - Uses type inference (no explicit types) for at least two variables
- Prints everything using string interpolation
void main() {
var language = 'Dart';
final birthYear = 2011; // Dart was born in 2011
const maxScore = 100;
// Try reassignment:
language = 'Flutter Dart'; // Works!
// birthYear = 2012; // Error: final can't be reassigned
// maxScore = 200; // Error: const can't be reassigned
var movies = ['Inception', 'Interstellar', 'The Matrix', 'Arrival', 'Dune'];
var populations = {
'Portland': 652503,
'Seattle': 749256,
'San Francisco': 873965,
};
print('I code in $language, born in $birthYear');
print('Max score is $maxScore');
print('My top movie: ${movies[0]}');
print('Portland population: ${populations["Portland"]}');
}
Exercise 2: null_safety.dart
Create a file that explores null safety:
- Declare a nullable
String?and a non-nullableString - Use the
??operator to provide a default - Use the
?.operator to safely call a method on a nullable variable - Use
??=to assign a value only when null - Demonstrate why
!is dangerous (with a comment explaining the risk)
void main() {
String? nickname;
String name = 'Campbell';
// ?? — default if null
print('Nickname: ${nickname ?? "none set"}');
// ?. — safe method call
print('Uppercase nickname: ${nickname?.toUpperCase()}'); // null
// ??= — assign if null
nickname ??= 'Cam';
print('Nickname after ??= : $nickname'); // Cam
// ! — dangerous assertion
String? maybeNull = 'I exist';
print(maybeNull!.toUpperCase()); // Works, but risky
// If maybeNull were null, the ! would crash the app at runtime
}
Exercise 3: functions.dart
Create a file that practices functions:
- Write a function
celsiusToFahrenheitusing arrow syntax - Write a function
describethat takes named parameters:required String name,int age = 0,String? hobby - Write a function
applyOperationthat takes two ints and aFunctionparameter, then applies that function - Use
List.map()with a lambda to double every number in a list
double celsiusToFahrenheit(double c) => c * 9 / 5 + 32;
String describe({
required String name,
int age = 0,
String? hobby,
}) {
var result = '$name, age $age';
if (hobby != null) {
result += ', enjoys $hobby';
}
return result;
}
int applyOperation(int a, int b, int Function(int, int) operation) {
return operation(a, b);
}
void main() {
print('100°C = ${celsiusToFahrenheit(100)}°F'); // 212.0°F
print(describe(name: 'Campbell', age: 22, hobby: 'coding'));
print(describe(name: 'Alice'));
print(applyOperation(10, 3, (a, b) => a + b)); // 13
print(applyOperation(10, 3, (a, b) => a * b)); // 30
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map((n) => n * 2).toList();
print('Doubled: $doubled'); // [2, 4, 6, 8, 10]
}
Don't just read these exercises — type them out. Seriously. Your fingers need to learn the syntax just as much as your brain does. Copy-pasting teaches you nothing. Typing it out, making typos, fixing them — that's where the learning happens.
📝 Module Quiz
Test what you learned. 45 seconds per question. Passing score is set per module. Your best attempt is saved in your browser so you can track progress -- nothing is sent to a server.
What's Next?
Teach: What the next module covers and why OOP is essential for Flutter. See: A preview of classes, constructors, inheritance, and mixins. Feel: Eager to level up from Dart basics to object-oriented patterns.
You now have the basics: variables, types, null safety, functions, and string interpolation. In Module 02, we level up to object-oriented programming in Dart — classes, constructors, inheritance, mixins, and more. This is where Dart really starts to shine, and it's essential for understanding how Flutter widgets work under the hood.
Here's a preview of why the next module matters. Every widget in Flutter is a class. Text is a class. Container is a class. MyAwesomeApp will be a class that you write. When you say class MyApp extends StatelessWidget, you're using every concept from Module 02 at once — inheritance, constructors, method overriding. If classes still feel fuzzy after Module 02, come back and re-read it. You will not regret the time investment.