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.
Flutter apps heavily rely on async data (API calls, navigation results, state restoration). Null safety makes these flows safer and more predictable.
Benefits include:
Dart distinguishes between non-nullable and nullable types.
String → can never be nullString? → may be nullThe compiler enforces correct usage and requires you to handle null cases explicitly.
String? name;
// print(name.length); ❌ compile-time error
print(name?.length); // ✅ safe? (nullable access)
Safely accesses a property or method.
user?.email! (null assertion)
Forces Dart to treat a value as non-null.
user!.emailIf 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.
Nested nullability is a common source of confusion.
List<String?> → the list exists, but items may be nullList<String>? → the list itself may be nullList<String?>? → the list may be null and may contain null itemsUnderstanding this distinction is essential when working with APIs and UI lists.
In Flutter, lists often contain conditionally added elements, especially in widget trees.
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]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.
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.
When working with classes, null safety behaves slightly differently than with local variables.
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
}This situation commonly appears when:
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.
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.
! instead of proper null checks.late to avoid initialization logic.null on first build.?? over !.10 min • Oct 27, 2025
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.
12 min • Jul 27, 2023
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.
11 min • Nov 25, 2025
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.