A shorts.
Suppose I make this cool simple widget with sound null safety:
import 'package:flutter/material.dart';
void main() {
return CoolWidget();
}
class CoolWidget extends StatelessWidget {
const CoolWidget({
this.text,
});
final String? text;
@override
Widget build(BuildContext build) {
if (text != null) return Text(text);
return Text('Y U NO GIVE TEXT?');
}
}
Flutter will get angry and summon this error log:
Error: The argument type 'String?' can't be assigned to the parameter type 'String' because 'String?' is nullable and 'String' isn't.
if (text != null) return Text(text);
^
It is silly that Flutter cannot dictate from the null check text != null
not a moment ago, and infer that the text
is in fact non-nullable. There is only one possible control flow from that point on!
There is bang operator (!
operator) to tell Flutter: “Yeah, I know it is typed as nullable, but I am super duper sure it will not be a null!”
Which sounds dangerous, no? Code changes, maintainer changes, the world is changing. There is a great chance that sometime in the future, a refactor done and the maintainer forget to deal with the bang operator.
Solution
After browsing the internet for a bit, I found that there was a mention in the Dart docs:
The analyzer can’t model the flow of your whole application, so it can’t predict the values of global variables or class fields.
which used to be in this page of docs, but currently not exists.
From that documentation text, implied that non-null inference can be done in a local scope.So if we define new local variable and do null check on that var, Flutter will gladly say: “OK homie, peace out ✌️”.
diff --git a/lib/main.dart b/lib/main.dart
index 96969696..69696969 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -13,7 +13,8 @@ class Cool Widget extends StatelessWidget {
@override
Widget build(BuildContext build) {
- if (text != null) return Text(text);
+ final String? newText = text;
+ if (newText != null) return Text(newText);
return Text('Y U NO GIVE TEXT?');
}
Final code looks like this.
import 'package:flutter/material.dart';
void main() {
return CoolWidget();
}
class CoolWidget extends StatelessWidget {
const CoolWidget({
this.text,
});
final String? text;
@override
Widget build(BuildContext build) {
final String? newText = text;
if (newText != null) return Text(newText);
return Text('Y U NO GIVE TEXT?');
}
}
Other Example
It’s not just variable (re)declaration, but passing parameter to a method also works.
For example, this will summon error:
import 'package:flutter/material.dart';
void main() {
return CoolWidget();
}
class CoolWidget extends StatelessWidget {
const CoolWidget({
this.text,
});
final String? text;
@override
Widget build(BuildContext build) {
return _buildBuildBuild();
}
Widget _buildBuildBuild() {
if (text != null) return Text(text);
return Text('Y U NO GIVE TEXT?');
}
}
You might have guessed that the solution is to pass the parameter to the private method:
diff --git a/lib/main.dart b/lib/main.dart
index 96969696..69696969 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -19,8 +19,8 @@ Widget build(BuildContext build) {
return _buildBuildBuild();
}
- Widget _buildBuildBuild() {
- if (text != null) return Text(text);
+ Widget _buildBuildBuild({String? newText}) {
+ if (newText != null) return Text(newText);
return Text('Y U NO GIVE TEXT?');
Conclusion
I personally like this approach, rather than using the bang operator (!
operator).
Will there be performance impact because we declare some new variables? There might be.
Is passing method parameter counts as declaring new variable? I honestly don’t know. Do tell me if you have insights about this 😃
Top comments (0)