Dart null safety doesn't work with class fields

Viewed 34477

I have migrated my Dart code to NNBD / Null Safety. Some of it looks like this:

class Foo {
  String? _a;
  void foo() {
    if (_a != null) {
      _a += 'a';
    }
  }
}

class Bar {
  Bar() {
    _a = 'a';
  }
  String _a;
}

This causes two analysis errors. For _a += 'a';:

An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.

For Bar() {:

Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.

In both cases I have already done exactly what the error suggests! What's up with that?

I'm using Dart 2.12.0-133.2.beta (Tue Dec 15).

Edit: I found this page which says:

The analyzer can’t model the flow of your whole application, so it can’t predict the values of global variables or class fields.

But that doesn't make sense to me - there's only one possible flow control path from if (_a != null) to _a += 'a'; in this case - there's no async code and Dart is single-threaded - so it doesn't matter that _a isn't local.

And the error message for Bar() explicitly states the possibility of initialising the field in the constructor.

4 Answers

The problem is that class fields can be overridden even if it is marked as final. The following example illustrates the problem:

class A {
  final String? text = 'hello';

  String? getText() {
    if (text != null) {
      return text;
    } else {
      return 'WAS NULL!';
    }
  }
}

class B extends A {
  bool first = true;

  @override
  String? get text {
    if (first) {
      first = false;
      return 'world';
    } else {
      return null;
    }
  }
}

void main() {
  print(A().getText()); // hello
  print(B().getText()); // null
}

The B class overrides the text final field so it returns a value the first time it is asked but returns null after this. You cannot write your A class in such a way that you can prevent this form of overrides from being allowed.

So we cannot change the return value of getText from String? to String even if it looks like we checks the text field for null before returning it.

An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.

It seems like this really does only work for local variables. This code has no errors:

class Foo {
  String? _a;
  void foo() {
    final a = _a;
    if (a != null) {
      a += 'a';
      _a = a;
    }
  }
}

It kind of sucks though. My code is now filled with code that just copies class members to local variables and back again. :-/

Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.

Ah so it turns out a "field initializer" is actually like this:

class Bar {
  Bar() : _a = 'a';
  String _a;
}

There are few ways to deal with this situation. I've given a detailed answer here so I'm only writing the solutions from it:

  • Use local variable (Recommended)

    void foo() {
      var a = this.a; // <-- Local variable
      if (a != null) {
        a += 'a';
        this.a = a;
      }
    }
    
  • Use ??

    void foo() {
      var a = (this.a ?? '') + 'a';
      this.a = a;
    }
    
  • Use Bang operator (!)

    You should only use this solution when you're 100% sure that the variable (a) is not null at the time you're using it.

    void foo() {
      a = a! + 'a'; // <-- Bang operator
    }
    

To answer your second question:

Non-nullable fields should always be initialized. There are generally three ways of initializing them:

  • In the declaration:

    class Bar {
      String a = 'a';
    }
    
  • In the initializing formal

    class Bar {
      String a;
    
      Bar({required this.a});
    }
    
  • In the initializer list:

    class Bar {
      String a;
    
      Bar(String b) : a = b;
    }
    

You can create your classes in null-safety like this

    class JobDoc {
  File? docCam1;
  File? docCam2;
  File? docBarcode;
  File? docSignature;

  JobDoc({this.docCam1, this.docCam2, this.docBarcode, this.docSignature});

  JobDoc.fromJson(Map<String, dynamic> json) {
    docCam1 = json['docCam1'] ?? null;
    docCam2 = json['docCam2'] ?? null;
    docBarcode = json['docBarcode'] ?? null;
    docSignature = json['docSignature'] ?? null;
  }
}
Related