Migration to Flutter Guide
Discover our battle-tested 21-step framework for a smooth and successful migration to Flutter!
Home
Glossary

Null safety in Dart

What is null safety in Dart?

Null safety is a Dart language feature that prevents variables from being null unless explicitly allowed. It helps eliminate a large class of runtime crashes by catching null-related issues at compile time instead of in production.

In practice, null safety forces you to clearly model whether "no value" is a valid state.

Why does it matter in Flutter app development?

Flutter apps heavily rely on async data (API calls, navigation results, state restoration). Null safety makes these flows safer and more predictable.

Benefits include:

  • fewer runtime crashes
  • clearer widget state contracts
  • better tooling and refactoring support
  • improved performance due to compiler optimizations

How does it work?

Dart distinguishes between non-nullable and nullable types.

  • String → can never be null
  • String? → may be null

The compiler enforces correct usage and requires you to handle null cases explicitly.

String? name;

// print(name.length); ❌ compile-time error
print(name?.length);   // ✅ safe

Key null safety operators

? (nullable access)

Safely accesses a property or method.

user?.email

! (null assertion)

Forces Dart to treat a value as non-null.

user!.email

If the value is null, the app will crash. Use sparingly.

?? (default value)

Provides a fallback when a value is null.

title ?? 'Unknown'

late

Defers initialization but promises the value will be set before use.

late String token;

Treat late as a promise. Breaking it results in a runtime error.

Null safety in collections (critical concept)

Nested nullability is a common source of confusion.

  • List<String?> → the list exists, but items may be null
  • List<String>? → the list itself may be null
  • List<String?>? → the list may be null and may contain null items

Understanding this distinction is essential when working with APIs and UI lists.

Conditional elements in lists (very common in Flutter UI)

In Flutter, lists often contain conditionally added elements, especially in widget trees.

Example

final widgets = [
  if (a != null) a,
  if (b != null) b,
  if (c != null) c,
];

Which can be also expressed like this:

final widgets = [?a, ?b, ?c]

Important rules

  • The list itself is not nullable.
  • Elements are only added if the condition is true.
  • No null values are inserted into the list.

This is not the same as List<Widget?>. Conditional elements keep the list clean and null-safe, which is especially important for Column, Row, and ListView.

Flow analysis and type promotion

Dart performs smart type promotion.

String? text;

if (text != null) {
  print(text.length); // ✅ text is promoted to String
}

Inside the if block, text is treated as non-null. Using text! here is unnecessary and considered poor style.

Nullable fields in classes and pattern matching

When working with classes, null safety behaves slightly differently than with local variables.

Example

class User {
 final String? name;
  User(this.name);
}

void printName(User user) {
  if (user.name != null) {
    print(user.name.length); // ❌ still an error in many cases
  }
}

Even though user.name != null, Dart cannot always promote user.name to String.

This is because class fields are considered potentially mutable (they may change between reads), so simple != null checks are not always sufficient.

The correct and safe approach is to bind the value to a local variable using pattern matching or destructuring.

Example using pattern matching:

if (user case User(:final name)) {
  print(name.length); // ✅ safe
}

Why this matters

This situation commonly appears when:

  • Working with API models.
  • Reading nullable fields from state objects.
  • Accessing data inside widgets.

Relying on ! in these cases hides real nullability problems and can lead to runtime crashes.

Using pattern matching or local binding makes your intent explicit and keeps the code truly null-safe.

JSON to Dart and null safety

API responses often contain optional fields, which should be reflected in your models.

class User {
  final String id;
  final String? email;

  User({required this.id, this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      email: json['email'] as String?,
    );
  }
}

In named constructors, non-nullable parameters must be marked required or have default values.

Common mistakes to avoid

  • Overusing ! instead of proper null checks.
  • Making everything nullable to silence the compiler.
  • Misusing late to avoid initialization logic.
  • Forgetting that async data may be null on first build.

Best practices

  • Make variables non-nullable by default.
  • Use nullable types only when null is a valid state.
  • Prefer type promotion and ?? over !.
  • Model API responses accurately.

Learn more

12 Flutter & Dart Code Hacks
by LeanCode

12 Flutter & Dart Code Hacks & Best Practices – How to Write Better Code?

In this article, we’re sharing LeanCode’s 12 practical Flutter and Dart patterns that help you write less boilerplate, make your code cleaner, and catch mistakes earlier. Apply these patterns and you'll find yourself coding faster, communicating more clearly with your teammates, and spending less time debugging issues that the compiler could have caught.

Flutter architecture by LeanCode

Feature-Based Flutter App Architecture - LeanCode

Read about the LeanCode approach to Flutter architecture. We highlight some of the design decisions that should be made when developing a feature in mobile apps. This includes our approach to dependency injection, state management, widget lifecycle, and data fetching.

4-Level Flutter Refactoring Framework by LeanCode

Taming Legacy Code in Flutter: Our Refactoring Framework for Enterprise Flutter Projects

Legacy code in Flutter? It happens, and it’s a sign of success. Fast growth creates technical debt. At LeanCode, we’ve helped enterprises untangle it with a proven framework that restores clarity, scalability, and speed without a costly rewrite.