In Dart, why does a null check on a nullable field often not promote it, and how do local copies or private final fields help?
answer
- a getter may return something else
- public fields can be overridden
- copy into a local, then check
- Dart 3.2: private final fields
- library-wide name conflicts block it
basics
~20 sIn Dart, a public field may be overridden by a getter and a non-final one reassigned between check and use, so flow analysis cannot promote it. Copy it into a local, or since Dart 3.2 make it private and final.
solid answer
~50 sPromotion needs proof that the value cannot change between the null check and the use. A local variable gives that proof; a field usually doesn't. A **public** field can be overridden in another library by a getter that returns something different on each call, and a **non-final** field can be reassigned by any code in between. So `if (person.middleName != null) person.middleName.length` fails to compile, and in Flutter so does checking `widget.middleName`. The standard fix is a local copy: `final middle = person.middleName; if (middle != null) …`. Since **Dart 3.2**, **private final** fields promote too, because privacy in Dart is per library, so the compiler can see every possible override. They still won't if a concrete getter or non-final field with the same name exists elsewhere in the library, or if the language version is below 3.2.
code
dart · 29 linesclass Person {
Person(this.first, this._middleName, this.last);
final String first;
final String? _middleName; // private + final: promotable since Dart 3.2
final String last;
String get fullName {
if (_middleName != null) return '$first $_middleName $last';
return '$first $last';
}
String get initials {
if (_middleName == null) return '${first[0]}${last[0]}';
return '${first[0]}${_middleName[0]}${last[0]}'; // promoted
}
}
class Contact {
Contact(this.first, this.middleName);
final String first;
final String? middleName; // public: never promotes
}
String greet(Contact c) {
// if (c.middleName != null) return c.middleName.trim(); // error: public property
final middle = c.middleName;
if (middle != null) return 'Hi ${c.first} ${middle.trim()}';
return 'Hi ${c.first}';
}go deeper
Recall that checking a field such as widget.middleName does not let you use it as non-null; copy it into a local first.
Explain why: overriding getters, reassignment and library-level privacy, and which private final fields promote since Dart 3.2 and which still do not.
Show you choose between a local copy, a private final field and a non-nullable redesign, and can diagnose a promotion that stopped after an unrelated change.
Weigh library boundaries and field visibility as part of API design, since they now decide what callers can promote without assertions.
## The failure ```dart class Person { Person(this.first, this.middleName, this.last); final String first; final String? middleName; final String last; } String label(Person p) { if (p.middleName != null) { return p.middleName.toUpperCase(); // ERROR } return p.first; } ``` The analyzer rejects the marked line with a message like **"'middleName' refers to a public property so it couldn't be promoted"**. The same thing happens in Flutter when a `State` checks `widget.middleName`, because that is a public field of the widget class. ## Why a field is not a local In Dart, reading `p.middleName` is a **member access**, and any member access might run a getter. For flow analysis to promote, the second read must be guaranteed to return the same value as the first. A field can break that guarantee in three ways: - **It is public.** A class in another library can `implements Person` or extend it and override `middleName` with a getter that returns a different value on every call. The compiler cannot see every library that will ever exist. - **It is not final.** Any code that runs between the check and the use — a method call, a callback — could assign `null` to it. - **It is a getter, not a field.** Getters are code, so their result can change, and they never promote. A **local variable** has none of these problems: nobody else can see it, and flow analysis can see every write to it. ## Fix 1: copy into a local The universal fix, and the one the Dart docs recommend first: ```dart String label(Person p) { final middle = p.middleName; if (middle != null) return middle.toUpperCase(); return p.first; } ``` `middle` is a local `String?` that promotes to `String`. If the code must also **change** the field, remember to write back to the field, not only to the local. ## Fix 2: private final fields (Dart 3.2+) Since **Dart 3.2**, a field that is both **private** (`_middleName`) and **final** promotes like a local — inside the class and from other code in the **same library**, because in Dart privacy is **per library**, not per class. Within one library the compiler can see every class that could override the name. A private final field still does **not** promote when: 1. another class in the same library declares a **concrete getter** or a **non-final field** with the same name, even an unrelated class; 2. a concrete class in the library has that name in its interface without implementing it, so the access would be forwarded to `noSuchMethod`; 3. the field is **external**; 4. the library's **language version** is below 3.2 — set by the `sdk` lower bound in `pubspec.yaml` or a `// @dart=` comment. ## Fix 3 and what not to do - A `!` on each use compiles, but it is a runtime assertion that the check already proved; it reads as noise and survives later refactors that break the proof. - Restructuring so a non-nullable value is passed in — a separate constructor argument or a validated model — removes the null altogether. ## Choosing | Situation | Fix | |---|---| | a public field or getter, e.g. `widget.middleName` | local copy | | your own class's private data | make it `final` and private, Dart 3.2+ | | a mutable private field | local copy; it cannot promote | | the null is not meaningful | redesign so the field is non-nullable |
- In Dart 3.2+, why can a private final field stop promoting when you add an unrelated class to the same file?Field promotion is decided library-wide. If any class in the library declares a concrete getter or non-final field with the same private name, some other class could extend one and implement the other, creating an override the compiler must respect. So the name is disqualified everywhere in that library.
- In Flutter, what is the idiomatic way to use a nullable widget.middleName inside a State's build method?Copy it into a local at the top of `build`: `final middle = widget.middleName;` then check `middle` and use it. `widget` is a getter on `State` and `middleName` is a public field of the widget, so neither can be promoted directly.
saying these in an interview costs you the question
- A final field always promotes after a null check, public or not.
- Field promotion does not exist in Dart, so ! is the only fix.
- Private in Dart means private to the class, so other classes cannot see _field.
- Making the field late is the standard way to get promotion.
- Marking the class final makes its public fields promote after a null check.