dart nullsaftey and using old packages - dart

I've enabled the dart 2.8 nullsaftey experiment.
I've converted my app to nullsaftey but its using an old pre-nullsafety package.
The problem is that the old package has a method which can return null:
/// Returns the environment variable with [name] or null if it doesn't
/// exist
String env(String name);
Which is used as follows:
var home = env('HOME');
If the HOME environment variable is missing, env returns null.
The problem is that env is declared as returning a String.
So when I write
var home = env('HOME');
home ??= '/home';
I get an error:
The operand can't be null, so the condition is always false.
Try removing the condition, an enclosing condition, or the whole conditional statement.
Given that all the nullsaftey release announcements say you can use nullsaftey with older packages, I'm guessing there is some way to declare an imported packages as non-nullsafe.
Problem is that I can't find any documentation on how to do this.

null safety has not been released yet! that is why you need to provide the experiment flag.
Language versioning
By default, whether or not null safety is supported in a library is determined by its language version. Any language version 2.8 or lower counts as opted out of null safety, and 2.9 or higher (subject to change) is opted in. The language version itself can come from one of two places:
The minimum bound of the package's declared SDK constraint. The following package will have a language version of 2.8.
name: foo
env:
sdk:
">=2.8.0 <3.0.0"
A language override comment at the top level of the file, before any other declarations. The following library will have a language version of 2.8.
// #dart=2.8
class Foo {}
The language override comment will take precedence over the SDK constraint, but only within the single library where it is declared.
Interaction between null safe and non-null safe code
The problem you are having is reproducible without different packages or incorrect language versions though, and has to do with the interaction between null-safe and non-null-safe code. Consider the following example:
// #dart=2.8
String foo() {
return null;
}
// #dart=2.9
import 'a.dart';
void main() {
var value = foo();
value ??= 'asd';
}
The return type of foo doesn't become String?, instead it gets tagged as String* - this is known as a legacy type. A legacy type is treated as a non-null type in opted in libraries. The goal of legacy types is to make it easier to migrate to null-safety through an in-order migration
Consider the example below:
// #dart=2.9
void foo(String value) {
// do something with non-null String.
}
// #dart=2.8
import 'a.dart';
void main() {
foo(getStringFromAPI());
}
While foo requires a non-null string, it isn't possible for the entry-point to actually pass it one - since it has not opted in yet. Without the treatment of legacy types as non-nullable types, it would not be possible to gradually migrate - because all libraries would need to be updated at once, or only updated to accept nullable types.
Out of order migration
By calling code that has not been migrated to null-safety from a null safe library, you are increasing the risk that you will be broken when that dependency eventually migrates. In you example, if home was treated as non-nullable then updating to a version of the dependency with an updated return value of String? would cause a compilation error.
For your specific case, I would recommend specifically annotating the type of home as String?. This is a perfectly valid type annotation, since in general T and T* are always assignable to T?. It also more correct, since you know the API can return null.
String? home = env('HOME');
home ??= '/home';
EDIT June 2021:
Null safety has released, yay! The first version of Dart with null safety enabled by default ended up being 2.12 and not 2.9 as documented in the question above.

Related

What is the ambiguate function doing?

This is a function in the main.dart file of the just_audio example. I don't understand what's going on with the "ambiguate" line. I understand the bang operator in this context casts to the "underlying type" but in this case there is no underlying type, I don't think. The underlying type is <T?>. I'm only familiar with what that means when I see it in documentation as "a type goes here." If it's in actual code, not sure what it's doing.
void initState() {
super.initState();
ambiguate(WidgetsBinding.instance)!.addObserver(this);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.black,
));
_init();
}
The ambiguate function from common.dart in the same lib folder:
T? ambiguate<T>(T? value) => value;
https://github.com/ryanheise/just_audio/tree/minor/just_audio/example/lib
The ambiguate function casts a value to its nullable type, so an int is cast to int?.
That allows using the ! (null assert) operator on the value without a warning, because a value of type int cannot be null, so you don't need to assert that it isn't.
The reason for doing so is that in a program which also contains non-null-safe code, the value can actually be null at runtime.
Or because you don't actually know whether the type will be int or int?, because it changed between versions of a library, and you don't want to lock yourself to only the newest version.
Which means that the only reason to use the function is that you expect your code to run against two different and incompatible versions of the same library, one null-safe or non-null-safe where a function can return null, and a newer null-safe version where the function cannot return null and is typed as such.
Then you can do ambiguate(uncertainValue)?.doSomething(), and it does something if the value isn't null, and it compiles against both versions without warning.
If you are not trying to make your code work against two different versions of the same library, which differ in nullability-behavior, then don't use ambiguate.
Even then, consider whether it'd just be easier to require the new version of the library, and lean into null safety.
(This particular use seems unnecessary. Doing ambiguate(something)!.method() will throw an error if the value is null, but so will something.method(), which will also not give any warnings. Well, unless the other version of the library is null safe and returns a nullable value, but then you shouldn't be using ! on it.)

Nullability mismatch in simple assignment after switching to sound null safety

I switched to sound null safety and started getting runtime error in a simple assignment, that should never happen with sound null safety:
final widgetOnPressed = widget.onPressed;
Error:
type '(LogData) => void' is not a subtype of type '((LogData?) => void)?'
I can repro it for Flutter versions 2.12.0-4.1.pre and 2.13.0-0.0.pre.505.
PR: https://github.com/flutter/devtools/pull/3971
Failing line: https://github.com/flutter/devtools/blob/9fc560ff2e6749459e2ca6a1dc00bf6fb16ed93b/packages/devtools_app/lib/src/shared/table.dart#L1184
To repro, start DevTools at this PR for macos, connect to an app and click the tab 'Logging'. DevTools will show red screen and error in console.
Is it dart bug or the app bug? If it is the app bug, how can I debug it?
It's a bug in your code.
You didn't say which kind of error you got - a compile-time error or a runtime error. I'm guessing runtime error. (Well, you did say to launch it in the debugger, so that is a good hint too.)
The line final widgetOnPressed = widget.onPressed; looks like it can't possibly fail. After all, the type of the local variable is inferred from the expression assigned to it, and the runtime value of that expression will surely be a subtype of the static type because the type system is sound!
Isn't it? ISN'T IT?
It's not, sorry. Dart 2's type system is mostly sound, even more so with null safety, but class generics is covariant, which can still be unsound. It's fairly hard to hit one of the cases where that unsoundness shows its ugly head, but returning a function where the argument type is the class's type variable is one.
Your state class extends State<TableRow<T?>>, so the widget getter returns a TableRow<T?>. The onPressed of that type has type ItemCallback<T?>?, aka, void Function(T?)?.
You create a _TableRowState<LogData>, with its widget which has static type TableRow<LogData?>, but you somehow manage to pass it a TableRow<LogData> instead. That's fine. Class generics are covariant, so all is apparently fine at compile-time.
Then you do final widgetOnPressed = widget.onPressed;.
The static type of widgetOnPressed is void Function(LogData?) here.
The actual runtime type of onPressed is void Function(LogData) because it's from a TableRow<LogData>.
A void Function(LogData) is-not-a void Function(LogData?) because the former cannot be used in all places where the latter can (in particular, it can't be used in a place where it's called with null).
This assignment is potentially unsound, and actually unsound in this case. The compiler knows this and inserts an extra check to ensure that you don't assign a value to the variable which isn't actually valid. That check triggers and throws the error you see.
How do you avoid that?
Don't create a TableRow<LogData> where a TableRow<LogData?> is required.
Or type the variable as:
final ItemCallback<T>? widgetOnPressed = widget.onPressed;
(no ? on the T).
Or rewrite everything to avoid returning a function with a covariant type parameter (from the class) occurring contra-variantly (as an argument type).
Which solution fits you depends on what you want to be able to do.

What is the right order to initialize class fields?

What is the logic here when the programmer initializes _random at once but the _streamController is initialized in the constructor?
Can all the fields be initialized without a constructor then?
RandomStore {
RandomStore() {
_streamController = StreamController<int>();
_timer = Timer.periodic(const Duration(seconds: 1),
(_) => _streamController.add(_random.nextInt(100)));
randomStream = ObservableStream(_streamController.stream);
}
late final Timer _timer;
final _random = Random();
late final StreamController<int> _streamController;
late final ObservableStream<int?> randomStream;
...
Can all the fields be initialized without a constructor ?
Yes, you can initialize all fields without having to declare a constructor, but only if you don't need a reference to the current instance (this) or if they are 'late' fields.
The determining factor in choosing where to initialize fields is whether or not you need to have the reference (even implicit) to this.
In Dart this is only available from the construcor body; this means in particular that this is not usable in the initializer list and inside the inline initializers (except for the late fields).For terminology, see Glossary below.
this is the reference to the current instance, and is required in order to read the instance fields, even if you usually omit it (e.g., in your snippet, randomStream is equivalent to this.randomStream).
For example, in your snippet, to initialize randomStream you need to be able to read the streamController field, so you have to mark it with late; thanks to late you can initialize randomStream in the constructor body or in the inline initializer (in this second case it will actually be initialized only when you try to access it for the first time; which is an advantage if its initialization is expensive and you want to avoid it as long as possible).
As an alternative to late, you could mark the field as nullable and initialize it in the constructor body (in which case the field will first be implicitly initialized with a null value; in fact this approach is not usable if the field is final and therefore cannot be reassigned).
Instead, to initialize the streamController field, you don't use a reference to this, so you could avoid the overhead of marking the field with late and you can initialize it in the initializer list or in the inline initializer (it is the same).
Example:
class RandomStore {
final StreamController<int> _streamController;
RandomStore()
: _streamController = StreamController<int>() {
}
}
Is late always a good choice? (UPDATED)
While from the above it might seem that 'late' is a great solution for most situations, the doc suggests avoiding 'late' if it is not really necessary, because:
It is less secure:
a late field (also if it has a non-nullable type) entails a risk of errors at runtime similar to that which occurred before the introduction of sound null safety, because Dart does not force you to perform any checks before reading its value (unlike nullable fields, for which access to properties requires the use of not null operator or conditional operator). Note that Dart does not offer the possibility to check if a late field has already been initialized (See Issue #324 mentioned below)
It adds overhead:
under the cover will be created a field with the indicated type, a field -presumably boolean- for keep track of whether the initialization has occurred, and a getter that at each access checks if the initialization had occurred.
Useful sources about late:
Doc Guide about null safety and late variables https://dart.dev/null-safety/understanding-null-safety#late-variables
Dart Best practice about late
https://dart.dev/guides/language/effective-dart/usage#dont-use-late-when-a-constructor-initializer-list-will-do
https://dart.dev/guides/language/effective-dart/usage#avoid-late-variables-if-you-need-to-check-whether-they-are-initialized
Dart issue #324 Should we provide a way to query the status of late variables?
a very interesting insight into 'late' (the discussion in which the Dart Team decided not to allow the final developers to check if a late field has been initialized)
Note the differences with Java:
(which personally made it difficult for me to switch from Java to Dart at first)
this in Dart is not available in inline initializers (in Java it is available)
the final fields in Dart must be initialized before the constructor body (in Java they can also be initialized in the constructor body)
Gloassary:
class MyClass {
MyClass(String v1, String v2)
: myField1 = v1, myField2 = v2 //This is the "initializer list"
//Compared to inline initializers, it allows you to initialize fields using the constructor arguments
{
//This is the "constructor body"
myField4 = myField1; //This operation require an implicit reference to `this`; it is equivalent to `myField4 = this.myField1;`
}
String myField1;
String myField2;
String myField3 = '3'; //This is the "inline initialization"
late String myField4;
}

Calling generic functions in Dart (2.0.0-dev.62.0)

Why does the following code even compile (Dart VM version: 2.0.0-dev.62.0):
int f<T>(T q) {
return q.hashCode;
}
void main() {
print(f<int>(23));
print(f<int>("wow"));
}
I thought f<A>(..) selects the A version of f?
The Dart VM does not use Dart 2 semantics by default when invoked directly yet (it does via Flutter, and is coming soon for Dart v2 dev), so you need to run with --preview-dart-2. If you do, you'll get an error:
Dannys-MacBook:lib danny$ dart --preview-dart-2 test.dart
test.dart:7:22: Error: A value of type 'dart.core::String' can't be assigned to a variable of type 'dart.core::int'.
Try changing the type of the left hand side, or casting the right hand side to 'dart.core::int'.
print(f<int>("wow"));

Conditional imports / code for Dart packages

Is there any way to conditionally import libraries / code based on environment flags or target platforms in Dart? I'm trying to switch out between dart:io's ZLibDecoder / ZLibEncoder classes and zlib.js based on the target platform.
There is an article that describes how to create a unified interface, but I'm unable to visualize that technique not creating duplicate code and redundant tests to test that duplicate code. game_loop employs this technique, but uses separate classes (GameLoopHtml and GameLoopIsolate) that don't seem to share anything.
My code looks a bit like this:
class Parser {
Layer parse(String data) {
List<int> rawBytes = /* ... */;
/* stuff you don't care about */
return new Layer(_inflateBytes(rawBytes));
}
String _inflateBytes(List<int> bytes) {
// Uses ZLibEncoder on dartvm, zlib.js in browser
}
}
I'd like to avoid duplicating code by having two separate classes -- ParserHtml and ParserServer -- that implement everything identically except for _inflateBytes.
EDIT: concrete example here: https://github.com/radicaled/citadel/blob/master/lib/tilemap/parser.dart. It's a TMX (Tile Map XML) parser.
You could use mirrors (reflection) to solve this problem. The pub package path is using reflection to access dart:io on the standalone VM or dart:html in the browser.
The source is located here. The good thing is, that they use #MirrorsUsed, so only the required classes are included for the mirrors api. In my opinion the code is documented very good, it should be easy to adopt the solution for your code.
Start at the getters _io and _html (stating at line 72), they show that you can load a library without that they are available on your type of the VM. Loading just returns false if the library it isn't available.
/// If we're running in the server-side Dart VM, this will return a
/// [LibraryMirror] that gives access to the `dart:io` library.
///
/// If `dart:io` is not available, this returns null.
LibraryMirror get _io => currentMirrorSystem().libraries[Uri.parse('dart:io')];
// TODO(nweiz): when issue 6490 or 6943 are fixed, make this work under dart2js.
/// If we're running in Dartium, this will return a [LibraryMirror] that gives
/// access to the `dart:html` library.
///
/// If `dart:html` is not available, this returns null.
LibraryMirror get _html =>
currentMirrorSystem().libraries[Uri.parse('dart:html')];
Later you can use mirrors to invoke methods or getters. See the getter current (starting at line 86) for an example implementation.
/// Gets the path to the current working directory.
///
/// In the browser, this means the current URL. When using dart2js, this
/// currently returns `.` due to technical constraints. In the future, it will
/// return the current URL.
String get current {
if (_io != null) {
return _io.classes[#Directory].getField(#current).reflectee.path;
} else if (_html != null) {
return _html.getField(#window).reflectee.location.href;
} else {
return '.';
}
}
As you see in the comments, this only works in the Dart VM at the moment. After issue 6490 is solved, it should work in Dart2Js, too. This may means that this solution isn't applicable for you at the moment, but would be a solution later.
The issue 6943 could also be helpful, but describes another solution that is not implemented yet.
Conditional imports are possible based on the presence of dart:html or dart:io, see for example the import statements of resource_loader.dart in package:resource.
I'm not yet sure how to do an import conditional on being on the Flutter platform.

Resources