1

I'm having to use the ! null-check override when accessing a field that has an optional constructor argument. I'm wondering if there's a better way to do this that I've not realized.

This optional, nullable field gets assigned a value in the constructor body if no argument is supplied.

class AuthService {
  http.Client? httpClient;
  
  AuthService({this.httpClient}) {
    httpClient ??= http.Client();
  }
}

Later when using httpClient I need to provide ! even though it will never be null...

http.Response response = await httpClient!.post(createUri,
          body: jsonEncode(requestBodyMap));

... otherwise the dart analyzer tells me: The method post cannot be unconditionally invoked because the receiver can be null.

When I use the late keyword, Lint checking is telling me the "assign-if-null" will never happen...

class AuthService {
  late http.Client httpClient;
  
  AuthService({this.httpClient}) {
    httpClient ??= http.Client(); // ← lint says this is useless & will never run
  }
}

... because httpClient can never be null.

Should I just ignore Lint?

Should I be doing something completely different?

nvoigt
  • 75,013
  • 26
  • 93
  • 142
Baker
  • 24,730
  • 11
  • 100
  • 106

2 Answers2

0
class AuthService {
  late http.Client httpClient;
  
  AuthService({this.httpClient}) {
    httpClient ??= http.Client(); // ← lint says this is useless & will > never run
  }
}

That shouldn't be valid. Since httpClient is non-nullable, AuthService({this.httpClient}) means that the AuthService constructor takes an optional named parameter that can't be null, which is a contradiction since optional parameters must be nullable.

At any rate, you should prefer to use initializer lists over constructor bodies when possible:

class AuthService {
  http.Client httpClient;
  
  AuthService({http.Client? httpClient})
    : httpClient = httpClient ?? http.Client();
}

Doing so avoids the need to need to use late (since you can initialize the member earlier).

Also see https://stackoverflow.com/a/64548861/ for more details about initializer lists vs. using constructor bodies.

lrn
  • 64,680
  • 7
  • 105
  • 121
jamesdlin
  • 81,374
  • 13
  • 159
  • 204
0

Thanks to examples this & that by Google Dart engineers and jamesdlin who had an answer here earlier, this seems to work:

  • use a never-null member / field instance variable: Client httpClient
  • don't use the syntactic sugar shortcut of AuthService({this.httpClient}) in constructor
    • in English this means: assign this.httpClient to our member var httpClient OR null if not provided
  • use regular, maybe-null constructor arg AuthService({Client? httpClient})
  • in initializer list, if maybe-null arg is not null, use it, else use default:
    • : httpClient = httpClient ?? http.Client();

So this:

Not so good

class AuthService {
  http.Client? httpClient;
  
  AuthService({this.httpClient}) {
    httpClient ??= http.Client();
  }
}

... becomes this:

Better

class AuthService {
  http.Client httpClient; // ← never null
  
  AuthService({http.Client? httpClient}) : httpClient = httpClient ?? http.Client(); 
  // ← no constructor body →
}

Initializer List

Just to be ultra clear on the initializer list line:

// ↓↓ class field/member instance, never-null
: httpClient = httpClient ?? http.Client();
// constructor arg ↑↑ supplied, can be null

In English: assign to our member the supplied client if not null, else instantiate a client & assign that.

Now the class never needs null checking on httpClient member or overrides like httpClient!.post().


I find initializer lists tricky because they can be hard to read and it's not obvious when they are executed (before constructor body, assigning values to member variables, without access to this as no instance exists yet).

Furthermore, constructor default args must always be constants. But initializer list assignments can be non-constant computed values.

So this is not allowed:

AuthService({this.httpClient = http.Client()})

... since http.Client() is not a constant. (If it were a constant constructor, it would be OK).

But this is fine:

AuthService({this.httpClient}) : httpClient = httpClient ?? http.Client();

It's also easy to visually confuse the member instance with supplied constructor arguments in initializer lists. Without syntax highlighting, it's hard to read. initializer list syntax highlighting in Android Studio

↑ purple = member ↑ grey = constructor arg

Check out jamesdlin's breakdown of inline vs. initializer list vs. constructor body for a better understanding of all this stuff.

Baker
  • 24,730
  • 11
  • 100
  • 106
  • This is weird, just after posting this, jamesdlin's answer re-appeared on this page. Anyways, leaving my long-winded reply here. – Baker Apr 08 '21 at 21:59