Beginner · Lesson 2 · 12 min read
Dart Essentials You Actually Need
The subset of Dart that Flutter code is written in: null safety, classes, collections, futures and async/await — without the parts you can skip.
Updated July 16, 2026
What you will learn
- Read and write null-safe Dart with confidence
- Use collection literals, spreads and collection-if
- Write classes with named parameters the way Flutter does
- Handle asynchronous work with Future, async and await
Dart is a small, boring language in the best sense: if you know Java, C#, Swift, Kotlin or TypeScript, you can read it immediately. This lesson covers only the features you will hit in your first month of Flutter.
Variables and null safety
Dart is sound null-safe: a variable cannot hold null unless its type ends with ?. The compiler proves this, so a String can never surprise you at runtime.
String name = 'Ada'; // can never be null
String? nickname; // may be null; defaults to null
final int year = 2026; // set once at runtime
const double pi = 3.14159; // compile-time constant
// var infers the type — still statically typed
var counter = 0; // int
counter = 'oops'; // compile errorTo use a nullable value you must handle the null case. Dart gives you several tools:
String? nickname;
// 1. Default value with ??
final display = nickname ?? 'no nickname';
// 2. Null-aware access — the whole expression is null if nickname is
final int? length = nickname?.length;
// 3. Null-aware assignment
nickname ??= 'Ace';
// 4. Promotion: inside the check, Dart knows it is non-null
if (nickname != null) {
print(nickname.toUpperCase());
}
// 5. The bang operator asserts non-null — it throws if you are wrong
final shout = nickname!.toUpperCase();Collections
final numbers = <int>[1, 2, 3];
final scores = <String, int>{'ada': 10, 'alan': 9};
final unique = <String>{'a', 'b', 'a'}; // Set — holds 'a', 'b'
// Spread another list in
final more = [0, ...numbers, 4];
// Conditionally include an item — used constantly in widget lists
final showAdmin = true;
final menu = [
'Home',
'Profile',
if (showAdmin) 'Admin',
for (final n in numbers) 'Item $n',
];
// Transform without loops
final doubled = numbers.map((n) => n * 2).toList();
final evens = numbers.where((n) => n.isEven).toList();
final total = numbers.fold<int>(0, (sum, n) => sum + n);Collection-if and collection-for matter more than they look. You will use them inside widget children lists to conditionally show UI without building intermediate lists.
Classes the Flutter way
Flutter code leans heavily on named parameters, required, and initialiser shorthand. Learn this shape and most Flutter APIs become obvious.
class Article {
const Article({
required this.title,
required this.author,
this.readMinutes = 5,
this.summary,
});
final String title;
final String author;
final int readMinutes; // has a default
final String? summary; // optional, may be null
// A named constructor
factory Article.draft(String title) =>
Article(title: title, author: 'unknown');
// Getters are computed properties, not methods
bool get isLongRead => readMinutes > 10;
// copyWith is the standard way to make a modified copy
Article copyWith({String? title, int? readMinutes}) {
return Article(
title: title ?? this.title,
author: author,
readMinutes: readMinutes ?? this.readMinutes,
summary: summary,
);
}
@override
String toString() => 'Article($title by $author)';
}
void main() {
const a = Article(title: 'Widgets 101', author: 'Ada');
final b = a.copyWith(readMinutes: 12);
print(b.isLongRead); // true
}Named parameters are why Flutter's deeply nested widget code stays readable: Padding(padding: ..., child: ...) tells you what each argument means at the call site.
Asynchronous Dart
A Future<T> is a value that will exist later — a network response, a file read, a database query. async/await lets you write it in a straight line.
Future<String> fetchGreeting() async {
await Future.delayed(const Duration(seconds: 1)); // simulate I/O
return 'Hello from the future';
}
Future<void> main() async {
print('start');
try {
final greeting = await fetchGreeting();
print(greeting);
} on TimeoutException {
print('took too long');
} catch (error, stackTrace) {
print('failed: $error');
} finally {
print('always runs');
}
// Run independent work concurrently instead of one after another
final results = await Future.wait([
fetchGreeting(),
fetchGreeting(),
]);
print(results.length); // 2
}A Stream<T> is the same idea for many values over time — websocket messages, sensor readings, database change notifications. You consume one with await for or, in the UI, with a StreamBuilder.
Stream<int> countTo(int max) async* {
for (var i = 1; i <= max; i++) {
await Future.delayed(const Duration(milliseconds: 300));
yield i;
}
}
Future<void> consume() async {
await for (final value in countTo(3)) {
print(value); // 1, then 2, then 3
}
}Patterns and switch expressions
Modern Dart has pattern matching, which shows up often in state handling. You do not need it on day one, but you will read it in other people's code.
sealed class LoadState {}
class Loading extends LoadState {}
class Success extends LoadState {
Success(this.items);
final List<String> items;
}
class Failure extends LoadState {
Failure(this.message);
final String message;
}
String describe(LoadState state) => switch (state) {
Loading() => 'Loading…',
Success(items: final items) => '${items.length} items',
Failure(message: final m) => 'Error: $m',
};Because LoadState is sealed, the compiler knows every subtype and will error if you forget a case. That exhaustiveness is what makes this pattern worth using for UI state.
Key takeaways
- Null safety is enforced by the compiler —
?marks nullable, and!should be rare. - Collection-if and collection-for let you build widget lists declaratively.
- Named parameters with
requiredare the standard Flutter class shape;copyWithis the idiom for immutable updates. Futureis one future value,Streamis many; useFuture.waitfor independent concurrent work.
Practice
Model a to-do item
Write a `Todo` class with a required `title`, an optional `note`, a `done` flag defaulting to false, and a `copyWith`. Then write an async function that returns `List<Todo>` after a one-second delay, and print only the unfinished ones.
Show hints
wherereturns a lazy iterable — call.toList()if you need a list.- Give the class a
constconstructor so instances can be reused.