blob: 9c5a6118bb2d5b29a827b82ac8ad02a4c3ee915a [file] [log] [blame] [view] [edit]
Dart web platform libraries e.g. `dart:html` is partially hand-written and
partially generated, with the code generation using the Chrome IDL as the source
of truth for many browser interfaces. This introduces a dependency on the
version of the IDL and doesnt always match up with other browser interfaces.
Currently, we do not intend on updating our scripts to use a newer version of
the IDL, so APIs and classes in these libraries may be inaccurate.
In order to work around this, we ask users to leverage JS interop. Longer term,
we intend to revamp our web library offerings to be more robust and reliable.
The following are workarounds to common issues you might see with using the web
platform libraries.
## Common Issues
### Missing/broken APIs
As mentioned above, there exists stale interfaces. While some of these may be
fixed in the source code, many might not.
In order to work around this, you can use the annotation `@staticInterop` from
`package:js`.
Lets look at an example. `FileReader` is a `dart:html` interface that is
missing the API `readAsBinaryString` ([#42834][]). We can work around this by
doing something like the following:
```dart
@JS()
library workarounds;
import 'dart:html';
import 'package:expect/legacy/async_minitest.dart';
import 'package:expect/expect.dart';
import 'package:js/js.dart';
@JS()
@staticInterop
class JSFileReader {}
extension JSFileReaderExtension on JSFileReader {
external void readAsBinaryString(Blob blob);
}
void main() async {
var reader = new FileReader();
reader.onLoad.listen(expectAsync((event) {
String result = reader.result as String;
Expect.equals(result, '00000000');
}));
var jsReader = reader as JSFileReader;
jsReader.readAsBinaryString(new Blob(['00000000']));
}
```
Alternatively, you can directly use the `js_util` library, using the methods
`getProperty`, `setProperty`, `callMethod`, and `callConstructor`.
```dart
import 'dart:html';
import 'dart:js_util' as js_util;
import 'package:expect/async_minitest.dart';
import 'package:expect/expect.dart';
void main() async {
var reader = new FileReader();
reader.onLoad.listen(expectAsync((event) {
String result = reader.result as String;
Expect.equals(result, '00000000');
}));
js_util.callMethod(reader, 'readAsBinaryString', [new Blob(['00000000'])]);
// We can manipulate properties as well.
js_util.setProperty(reader, 'foo', 'bar'); // reader.foo is now ‘bar’
Expect.equals(js_util.getProperty(reader, 'foo'), 'bar');
}
```
In the case where the API is missing a constructor, we can define a constructor
within a `@staticInterop` class. Note that constructors, `external` or
otherwise, are disallowed in extensions currently. For example:
```dart
@JS()
library workarounds;
import 'dart:js_util' as js_util;
import 'package:expect/expect.dart';
import 'package:js/js.dart';
@JS('KeyboardEvent')
@staticInterop
class JSKeyboardEvent {
external JSKeyboardEvent(String typeArg, Object keyboardEventInit);
}
extension JSKeyboardEventExtension on JSKeyboardEvent {
external String get key;
}
void main() {
var event = JSKeyboardEvent('KeyboardEvent',
js_util.jsify({'key': 'A'}));
Expect.equals(event.key, 'A');
}
```
or with `js_util`'s `callConstructor`:
```dart
import 'dart:html';
import 'dart:js_util' as js_util;
import 'package:expect/expect.dart';
void main() {
List<dynamic> eventArgs = <dynamic>[
'KeyboardEvent',
<String, dynamic>{'key': 'A'}
];
KeyboardEvent event = js_util.callConstructor(
js_util.getProperty(window, 'KeyboardEvent'), js_util.jsify(eventArgs));
Expect.equals(event.key, 'A');
}
```
### Private/unimplemented native types
There are several native interfaces that are suppressed e.g.
`USBDevice` ([#42200][]) due to historical reasons. These native interfaces are
marked with `@Native`, are private, and have no attributes associated with them.
Therefore, unlike other `@Native` objects, we can’t access any of the APIs or
attributes associated with this interface. We can again either use the
`@staticInterop` annotation or use the `js_util` library to circumvent this
issue. For example, we can abstract a `_SubtleCrypto` object:
```dart
@JS()
library workarounds;
import 'dart:html';
import 'dart:js_util' as js_util;
import 'dart:typed_data';
import 'package:js/js.dart';
@JS()
external Crypto get crypto;
@JS()
@staticInterop
class JSSubtleCrypto {}
extension JSSubtleCryptoExtension on JSSubtleCrypto {
external dynamic digest(String algorithm, Uint8List data);
Future<ByteBuffer> digestFuture(String algorithm, Uint8List data) =>
js_util.promiseToFuture(digest(algorithm, data));
}
void main() async {
var subtle = crypto.subtle! as JSSubtleCrypto;
var digest = await subtle.digestFuture('SHA-256', Uint8List(16));
}
```
or with `js_util`:
```dart
@JS()
library workarounds;
import 'dart:html';
import 'dart:js_util' as js_util;
import 'dart:typed_data';
import 'package:js/js.dart';
@JS()
external Crypto get crypto;
void main() async {
var subtle = crypto.subtle!;
var array = Uint8List(16);
var promise = js_util.promiseToFuture<ByteBuffer>(js_util
.callMethod(subtle, 'digest', ['SHA-256', array])); // SubtleCrypto.digest
var digest = await promise;
}
```
## Workarounds to Avoid
### Using non-`@staticInterop` `package:js` types
Avoid casting these native objects to non-`@staticInterop` `package:js` types
e.g.
```dart
@JS()
library workarounds;
import 'dart:html';
import 'package:js/js.dart';
@JS()
external Crypto get crypto;
@JS()
class SubtleCrypto {}
void main() {
SubtleCrypto subtle = crypto.subtle! as SubtleCrypto;
}
```
With the above, you’ll see a static error:
`Error: Non-static JS interop class 'SubtleCrypto' conflicts with natively supported class '_SubtleCrypto' in 'dart:html'.`
This is because the types in the `@Native` annotation are reserved and the above
leads to namespace conflicts between the `@Native` type and the user JS interop
type in the compiler. `@staticInterop` classes, however, don't have this issue.
### Using extensions on `@Native` types
One alternative that seems viable is to use a static extension on the `@Native`
type in `dart:html` directly, e.g.
```dart
extension FileReaderExtension on FileReader {
external void readAsBinaryString(Blob blob);
}
```
This may work fine, as long as `FileReader` does not add `readAsBinaryString`.
In the case where this API is added to the class, Dart will [prioritize][] that
instance method over the extension method you wrote. This may lead to issues,
like a type error when the signatures between the two methods are incompatible,
or confusing runtime behavior.
Furthermore, you may come across API conflicts with other users who have also
defined extension methods on these `@Native` types.
To avoid the above, it's recommended you stick with `@staticInterop`.
In the future, when views/extension types are introduced to the language, this
guidance will likely change so that you can directly use views on `@Native`
types.
[#42834]: https://github.com/dart-lang/sdk/issues/42834
[#42200]: https://github.com/dart-lang/sdk/issues/42200
[prioritize]: https://github.com/dart-lang/language/blob/master/accepted/2.7/static-extension-methods/feature-specification.md#member-conflict-resolution