dart - correct coding pattern for subscription when using null saftey?

Viewed 115

I've enabled the dart 2.8 experimental null saftey.

I have the following exiting code.

StreamSubscription<String> subscription;
    subscription =
        response.transform(Utf8Decoder()).transform(LineSplitter()).listen(
      (line) async {
        result += line;
      },
      onDone: () async {
        unawaited(subscription.cancel());
        completer.complete(result);
      },
    );

With null saftey enabled I get a error in the 'onDone' method where it calls subscription.cancl

"The expression is nullable and must be null-checked before it can be used.
Try checking that the value isn't null before using it.",

I can fix the problem by putting a conditional before the call to cancel, but this seems unnecessary as in reality subscription can never be null.

Is there a coding pattern that allows subscription to be declared as non-null?

1 Answers

The problem here is that the read of subscription happens at a place where it's still potentially unassigned. It isn't, actually, but we only know that because the listen method promises not to call any of the callbacks before returning. The compiler can't see that. So, you need to move the reading to after the assignment.

What I'd do to make this listen call work:

var buffer = StringBuffer(result);
var subscription = response
  .transform(Utf8Decoder())
  .transform(LineSplitter())
  .listen((line) {
    buffer.write(line);
  });
subscription.onDone(() {
    completer.complete(buffer.toString());
  });

I removed the async from the callbacks because it is not needed. All it does to make these functions async is to return a future that no-one would ever look at. In general, the callbacks on Stream and Future should have non-async callbacks.

I also removed the subscription.cancel from the onDone event handler. If you get a "done" event, the subscription is done, there is no need to cancel it.

I also added a string buffer to avoid the quadratic time and space complexity of repeated string concatenation.

Looking at the code, you seem to be concatenating lines right after splitting them, maybe all you need is:

  response.transform(Utf8Decoder()).join("").then(completer.complete);

I'll assume for now that the splitting+joining is necessary.

In that case, what I'd actually prefer to do instead is of using listen is:

var buffer = StringBuffer();
response
  .transform(Utf8Decoder())
  .transform(LineSplitter())
  .forEach((line) {
    buffer.write(line);
  }).then(() {
    completer.complete(buffer.toString());
  }, onError: (e, s) {
    completer.completeError(e, s);
  });

or, if in an async function:

try {
  var buffer = StringBuffer();
  await for (var line in response.transform(Utf8Decoder()).transform(LineSplitter())) {
    buffer.write(line);
  }
  completer.complete(buffer.toString());  
} catch(e, s) {
  completer.completeError(e, s);
}
Related