Take the following non-null safe Dart code:
static String appBarShiftTitleString(int fromEpochSeconds) {
String monthWord;
String dayWord;
DateTime dt = DateTime.fromMillisecondsSinceEpoch(fromEpochSeconds * 1000);
switch (dt.month) {
case 1:
monthWord = "Jan";
break;
case 2:
monthWord = "Feb";
break;
case 3:
monthWord = "Mar";
break;
case 4:
monthWord = "Apr";
break;
case 5:
monthWord = "May";
break;
case 6:
monthWord = "Jun";
break;
case 7:
monthWord = "Jul";
break;
case 8:
monthWord = "Aug";
break;
case 9:
monthWord = "Sep";
break;
case 10:
monthWord = "Oct";
break;
case 11:
monthWord = "Nov";
break;
case 12:
monthWord = "Dec";
break;
}
switch (dt.weekday) {
case 1:
dayWord = "Mon";
break;
case 2:
dayWord = "Tue";
break;
case 3:
dayWord = "Wed";
break;
case 4:
dayWord = "Thu";
break;
case 5:
dayWord = "Fri";
break;
case 6:
dayWord = "Sat";
break;
case 7:
dayWord = "Sun";
break;
}
return dayWord + ' ' + monthWord + ' ' + dt.day.toString();
}
Android Studio is saying, "The non-nullable local variable 'dayWord' must be assigned before it can be used."
I understand the error and have discovered that I can simply modify the first two lines of the method like this:
String monthWord = "error!";
String dayWord = "error!";
This way, I satisfy the language rules, and it will be plainly obvious if we reach what ought to be an impossible situation of the variable not having been assigned.
This seems hacky though... so in these types of scenarios, what is the elegant and proper way to convert this code to null safety, and if there are multiple ways, then what are the pros and cons?
In general, you have a few options:
assert
later:String monthWord = '';
// ...
switch (dt.month) {
// ...
}
assert(monthWord.isNotEmpty);
This will cause debug builds to throw AssertionError
at runtime if you neglect to handle a case for it in the switch
.
String? monthWord;
// ...
switch (dt.month) {
// ...
}
monthWord!;
// Since `monthWord` is a local variable, it will now be promoted to a
// non-nullable `String` type.
This will throw a TypeError
in all build types if you neglect to set the variable to a non-null value.
late
Declaring variables as late
states that you promise that the variables will be initialized before they are ever read. The compiler will generate runtime checks that verify that the variable is initialized when you try to access it. This will throw a LateInitializationError
in all build types if you neglect to set the variable.
default
case that throwsIf all of your case
s set a local variable, adding a default
case that throws allows the compiler to deduce that that variable must always be set if code after the switch
statement is reached:
String monthWord; // No explicit initialization required!
// ...
switch (dt.month) {
case 1:
monthWord = "Jan";
break;
// ... etc. ...
default:
throw AssertionError('Unhandled case: ${dt.month}');
}
// The compiler now can deduce that `monthWord` is guaranteed to be
// initialized.
(Note that you should not add a default
case for this purpose if you're using a switch
statement on an enum
type. For enum
s, the compiler and analyzer can determine if your case
s are exhaustive and will generate analysis warnings if you accidentally omit any cases.)
As for which approach to use, it's mostly a matter of preference. They're all mostly equivalent in that they'll result in runtime errors. I personally would choose #1 (assert
) or #4 (default
case) to avoid unnecessary checks in release builds.
In your particular example, I also would just use DateTime.month
and DateTime.day
as indices into List
s of the month and day names respectively:
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
assert(months.length == 12);
assert(days.length == 7);
var monthWord = months[dt.month - 1];
var dayWord = days [dt.day - 1];