EthicsComputerCourses
Join training +91 90532 10052

Variables & Sound Null Safety

Flutter & Dart · Lesson 2 of 29 Beginner 7 min read Updated

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:

Dart
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');
}
Output
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:

  • final means "set once". The value can be calculated while the program runs.
  • const means "a compile-time constant": the value must be known before the program runs, such as a number or a fixed string.
Dart
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);
}
Output
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.

Dart
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
}
Output
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:

Text
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:

OperatorNameMeaning
a?.bnull-aware accessUse b only if a is not null, otherwise the result is null
a ?? bif-nullThe value of a, or b if a is null
a ??= bnull-aware assignmentAssign b to a only if a is null
a!null assertion"I promise a is not null"; crashes if it is
Dart
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);
}
Output
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:

Dart
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;
}
Output
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 an if check.
  • 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 final fields, 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 const for values known only at run time, such as DateTime.now() or user input. Use final.

Key points

  • var infers the type; once set, the type never changes.
  • final is set once at run time; const is a compile-time constant.
  • Types are non-nullable by default; add ? to allow null.
  • Flow analysis promotes String? to String after a null check.
  • ?., ?? and ??= handle null safely; ! asserts and can crash.
  • late delays 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
Dart
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}');
}
Output
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?

2. What is the difference between final and const?

3. What does int.tryParse('abc') ?? 0 give?

4. When is the initializer of late final x = compute(); run?

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