Variables & Sound Null Safety
A variable is a named box that holds a value, such as a student's name or a train fare. Dart gives you a few ways to create variables, and each one tells the reader something: can this value change, is it known before the app runs, can it be missing?
The second half of this lesson covers sound null safety, one of Dart's most important features. It stops a whole family of crashes before your app ever reaches a phone, and you will use it in every Flutter screen you write.
Declaring variables
You can let Dart work out the type with var, or write the type yourself:
void main() {
var studentName = 'Ananya'; // Dart infers String
int rollNumber = 23;
double fee = 1299.50;
bool hasPaid = false;
print('$studentName (roll $rollNumber) owes ₹$fee. Paid: $hasPaid');
rollNumber = 24; // a var or typed variable can change
hasPaid = true;
print('$studentName (roll $rollNumber) paid: $hasPaid');
}Ananya (roll 23) owes ₹1299.5. Paid: false Ananya (roll 24) paid: true
Once Dart has decided the type, it stays fixed. studentName = 42; would be a compile error, because studentName is a String. This is what type safe means. Notice also that 1299.50 prints as 1299.5: a double does not remember trailing zeros. You will learn how to format money in Data Types & Operators.
Dart style uses lowerCamelCase for variable names: studentName, not student_name or StudentName.
final and const
Most values in a program never change after they are set. Mark them so the compiler can stop accidental changes:
finalmeans "set once". The value can be calculated while the program runs.constmeans "a compile-time constant": the value must be known before the program runs, such as a number or a fixed string.
void main() {
const gstPercent = 18; // known before the program runs
const shopName = 'Rohtak Mobile Store';
final price = 15000;
final gst = price * gstPercent / 100; // calculated at run time
final total = price + gst;
print(shopName);
print('Price: $price, GST: $gst, Total: $total');
final today = DateTime.now(); // final is fine: set once, at run time
print(today.year >= 2026);
}Rohtak Mobile Store Price: 15000, GST: 2700.0, Total: 17700.0 true
Trying to assign price = 16000; later gives a compile error: a final variable can only be set once. Writing const today = DateTime.now(); is also an error, because the current time is not known until the program runs.
A good habit: use final by default, const for fixed values, and var only when the value really needs to change. In Flutter you will write const in front of widgets very often, because constant widgets can be reused instead of rebuilt.
Sound null safety
null means "no value". In many languages any variable can secretly be null, and using it crashes the program. Dart fixes this with null safety: a normal type such as String can never hold null. To allow null, add ? to the type.
void main() {
String city = 'Jaipur';
String? nickname; // nullable: starts as null
print(city.length);
print(nickname);
nickname = 'Ronnie';
print(nickname.length); // allowed: Dart knows it is not null here
}6 null 6
If you write city = null; or call nickname.length before giving nickname a value, the program does not compile. The Dart compiler prints messages such as these:
Error: A value of type 'Null' can't be assigned to a variable of type 'String'.
Error: Property 'length' cannot be accessed on 'String?' because it is potentially null.
Try accessing using ?. instead.The word sound means the guarantee is complete: if a variable's type is non-nullable, it can never be null while the program runs. The compiler checks this, and it can also make your code faster because it skips null checks that cannot fail.
In the last line of the example, nickname.length works because Dart's flow analysis sees that nickname was just given a string. Inside an if (x != null) block the same thing happens. This is called type promotion: Dart promotes String? to String where it can prove the value is not null.
Null-aware operators
Checking for null with if everywhere would be tiring. Dart has short operators for the common cases:
| Operator | Name | Meaning |
|---|---|---|
a?.b | null-aware access | Use b only if a is not null, otherwise the result is null |
a ?? b | if-null | The value of a, or b if a is null |
a ??= b | null-aware assignment | Assign b to a only if a is null |
a! | null assertion | "I promise a is not null"; crashes if it is |
String? couponFor(String name) => name == 'Isha' ? 'first50' : null;
String? findPin(String city) => city == 'Rohtak' ? '124001' : null;
void main() {
final kabir = couponFor('Kabir');
print(kabir?.toUpperCase());
print(couponFor('Isha')?.toUpperCase());
print(kabir ?? 'NO-COUPON');
var applied = couponFor('Kabir');
applied ??= 'DIWALI10'; // was null, so it gets a value
print(applied);
var ishaCoupon = couponFor('Isha');
ishaCoupon ??= 'DIWALI10'; // ignored: Isha already has a coupon
print(ishaCoupon);
final typed = '3x';
final quantity = int.tryParse(typed) ?? 1;
print('Quantity: $quantity');
print(findPin('Rohtak')!.length);
}null FIRST50 NO-COUPON DIWALI10 first50 Quantity: 1 6
couponFor returns a String?, so every use must deal with the null case. int.tryParse also returns a nullable value, an int?: a number, or null if the text is not a valid number. Pairing it with ?? is the safe way to read numbers that users type. findPin('Rohtak') returns a String?, and the ! tells Dart "I know this call will not return null here". Use ! rarely. If you are wrong, the app stops with an error at run time, which is exactly what null safety tries to avoid.
late variables
Sometimes a non-nullable variable cannot get its value at the moment you declare it, but you know it will be set before anyone reads it. Mark it late:
late String welcomeMessage;
void setUp(String name) {
welcomeMessage = 'Welcome back, $name!';
}
void main() {
setUp('Farhan');
print(welcomeMessage);
late final int seats = countSeats(); // runs only when first read
print('Before reading seats');
print(seats);
}
int countSeats() {
print('Counting seats...');
return 72;
}Welcome back, Farhan! Before reading seats Counting seats... 72
late moves the check from compile time to run time: reading a late variable before it is set throws a LateInitializationError. A late variable with an initializer is lazy: countSeats() only runs when seats is first used. In Flutter you will meet late in State classes, where a controller is created in initState().
Common mistakes
- Adding
!to silence the analyzer. It hides the problem until the app crashes. Prefer??,?.or anifcheck. - Making everything nullable. Use
String?only when "no value" is a real possibility, such as an optional middle name. - Expecting promotion on a public field. Dart can promote local variables and private
finalfields, but not a normal public field of an object, because it could change between the check and the use. Copy it into a local variable first. - Using
constfor values known only at run time, such asDateTime.now()or user input. Usefinal.
Key points
varinfers the type; once set, the type never changes.finalis set once at run time;constis a compile-time constant.- Types are non-nullable by default; add
?to allownull. - Flow analysis promotes
String?toStringafter a null check. ?.,??and??=handlenullsafely;!asserts and can crash.latedelays initialisation and checks it at run time.
Exercise
Write a program for a cinema in Lucknow. A ticket costs ₹250 (a const). Store the number of tickets typed by a user as the string '4' and convert it with int.tryParse, using 1 if conversion fails. Store an optional coupon code in a String? that starts as null. Print the total, then print the coupon or 'none' using ??. Then change the typed text to 'four' and check that the program uses 1 ticket.
Show solution
void main() {
const ticketPrice = 250;
final typed = '4';
final tickets = int.tryParse(typed) ?? 1;
String? coupon;
print('Tickets: $tickets, total: ₹${tickets * ticketPrice}');
print('Coupon: ${coupon ?? 'none'}');
final badTyped = 'four';
final fallback = int.tryParse(badTyped) ?? 1;
print('Tickets: $fallback, total: ₹${fallback * ticketPrice}');
}Tickets: 4, total: ₹1000 Coupon: none Tickets: 1, total: ₹250
int.tryParse returns null for 'four', and ?? 1 replaces it with a sensible default. The coupon is null, so ?? prints 'none'.
Quick Quiz
1. Which declaration can hold null?
Only a nullable type, written with ?, can hold null.
2. What is the difference between final and const?
DateTime.now() can be final but not const, because it is only known when the program runs.
3. What does int.tryParse('abc') ?? 0 give?
tryParse returns null for invalid text, and ?? replaces it with 0.
4. When is the initializer of late final x = compute(); run?
A late variable with an initializer is lazy: it runs on first use.
Learn Flutter & Dart with a mentor
Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
