dartdart-null-safetynull-safety

How best to convert old Dart code that triggers "The non-nullable variable must be assigned" error?


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?


Solution

  • In general, you have a few options:

    1. Initialize the variable to some non-null sentinel value and 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.

    2. Make the variable nullable and use the null assertion operator:

    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.

    3. Make the variable 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.

    4. Add a default case that throws

    If all of your cases 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 enums, the compiler and analyzer can determine if your cases 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 Lists 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];