null safety (#154)

Co-authored-by: Nate Bosch <nbosch@google.com>
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index 6d84019..3ddce8c 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -14,7 +14,7 @@
 
 jobs:
   # Check code formatting and static analysis on a single OS (linux)
-  # against Dart dev and stable.
+  # against Dart dev.
   analyze:
     runs-on: ubuntu-latest
     strategy:
@@ -42,7 +42,8 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest]
-        sdk: [stable, dev]
+        # TODO: beta -> 2.12.0 once it's stable!
+        sdk: [beta, dev]
     steps:
       - uses: actions/checkout@v2
       - uses: dart-lang/setup-dart@v0.5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a83f991..261415e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.0.0-dev
+
+- Support null safety.
+- Require Dart 2.12.
+
 ## 1.2.0
 
 * Add `protocols` argument to `WebSocketChannel.connect`. See the docs for
diff --git a/lib/html.dart b/lib/html.dart
index 7afaf94..1694eb8 100644
--- a/lib/html.dart
+++ b/lib/html.dart
@@ -19,31 +19,31 @@
   final WebSocket _webSocket;
 
   @override
-  String get protocol => _webSocket.protocol;
+  String? get protocol => _webSocket.protocol;
 
   @override
-  int get closeCode => _closeCode;
-  int _closeCode;
+  int? get closeCode => _closeCode;
+  int? _closeCode;
 
   @override
-  String get closeReason => _closeReason;
-  String _closeReason;
+  String? get closeReason => _closeReason;
+  String? _closeReason;
 
   /// The number of bytes of data that have been queued but not yet transmitted
   /// to the network.
-  int get bufferedAmount => _webSocket.bufferedAmount;
+  int? get bufferedAmount => _webSocket.bufferedAmount;
 
   /// The close code set by the local user.
   ///
   /// To ensure proper ordering, this is stored until we get a done event on
   /// [_controller.local.stream].
-  int _localCloseCode;
+  int? _localCloseCode;
 
   /// The close reason set by the local user.
   ///
   /// To ensure proper ordering, this is stored until we get a done event on
   /// [_controller.local.stream].
-  String _localCloseReason;
+  String? _localCloseReason;
 
   @override
   Stream get stream => _controller.foreign.stream;
@@ -51,8 +51,7 @@
       StreamChannelController(sync: true, allowForeignErrors: false);
 
   @override
-  WebSocketSink get sink => _sink;
-  WebSocketSink _sink;
+  late final WebSocketSink sink = _HtmlWebSocketSink(this);
 
   /// Creates a new WebSocket connection.
   ///
@@ -66,14 +65,12 @@
   /// binary messages to be delivered as [Uint8List]s. If it's
   /// [BinaryType.blob], they're delivered as [Blob]s instead.
   HtmlWebSocketChannel.connect(url,
-      {Iterable<String> protocols, BinaryType binaryType})
+      {Iterable<String>? protocols, BinaryType? binaryType})
       : this(WebSocket(url.toString(), protocols)
           ..binaryType = (binaryType ?? BinaryType.list).value);
 
   /// Creates a channel wrapping [_webSocket].
   HtmlWebSocketChannel(this._webSocket) {
-    _sink = _HtmlWebSocketSink(this);
-
     if (_webSocket.readyState == WebSocket.OPEN) {
       _listen();
     } else {
@@ -134,7 +131,7 @@
         super(channel._controller.foreign.sink);
 
   @override
-  Future close([int closeCode, String closeReason]) {
+  Future close([int? closeCode, String? closeReason]) {
     _channel._localCloseCode = closeCode;
     _channel._localCloseReason = closeReason;
     return super.close();
diff --git a/lib/io.dart b/lib/io.dart
index 0ec108f..f90e103 100644
--- a/lib/io.dart
+++ b/lib/io.dart
@@ -19,16 +19,16 @@
   ///
   /// If the channel was constructed with [IOWebSocketChannel.connect], this is
   /// `null` until the [WebSocket.connect] future completes.
-  WebSocket _webSocket;
+  WebSocket? _webSocket;
 
   @override
-  String get protocol => _webSocket?.protocol;
+  String? get protocol => _webSocket?.protocol;
 
   @override
-  int get closeCode => _webSocket?.closeCode;
+  int? get closeCode => _webSocket?.closeCode;
 
   @override
-  String get closeReason => _webSocket?.closeReason;
+  String? get closeReason => _webSocket?.closeReason;
 
   @override
   final Stream stream;
@@ -55,11 +55,11 @@
   /// [WebSocketChannelException] wrapping that error and then closes.
   factory IOWebSocketChannel.connect(
     Object url, {
-    Iterable<String> protocols,
-    Map<String, dynamic> headers,
-    Duration pingInterval,
+    Iterable<String>? protocols,
+    Map<String, dynamic>? headers,
+    Duration? pingInterval,
   }) {
-    IOWebSocketChannel channel;
+    late IOWebSocketChannel channel;
     final sinkCompleter = WebSocketSinkCompleter();
     final stream = StreamCompleter.fromFuture(
       WebSocket.connect(url.toString(), headers: headers, protocols: protocols)
@@ -104,6 +104,6 @@
         super(webSocket);
 
   @override
-  Future close([int closeCode, String closeReason]) =>
+  Future close([int? closeCode, String? closeReason]) =>
       _webSocket.close(closeCode, closeReason);
 }
diff --git a/lib/src/_connect_api.dart b/lib/src/_connect_api.dart
index c816181..c58910e 100644
--- a/lib/src/_connect_api.dart
+++ b/lib/src/_connect_api.dart
@@ -10,6 +10,6 @@
 /// communicate over the resulting socket.
 ///
 /// The optional [protocols] parameter is the same as `WebSocket.connect`.
-WebSocketChannel connect(Uri uri, {Iterable<String> protocols}) {
+WebSocketChannel connect(Uri uri, {Iterable<String>? protocols}) {
   throw UnsupportedError('No implementation of the connect api provided');
 }
diff --git a/lib/src/_connect_html.dart b/lib/src/_connect_html.dart
index 5839c41..e725d1b 100644
--- a/lib/src/_connect_html.dart
+++ b/lib/src/_connect_html.dart
@@ -12,5 +12,5 @@
 /// communicate over the resulting socket.
 ///
 /// The optional [protocols] parameter is the same as `WebSocket.connect`.
-WebSocketChannel connect(Uri uri, {Iterable<String> protocols}) =>
+WebSocketChannel connect(Uri uri, {Iterable<String>? protocols}) =>
     HtmlWebSocketChannel.connect(uri, protocols: protocols);
diff --git a/lib/src/_connect_io.dart b/lib/src/_connect_io.dart
index 5e6bcd8..a7a82c4 100644
--- a/lib/src/_connect_io.dart
+++ b/lib/src/_connect_io.dart
@@ -11,5 +11,5 @@
 /// communicate over the resulting socket.
 ///
 /// The optional [protocols] parameter is the same as `WebSocket.connect`.
-WebSocketChannel connect(Uri uri, {Iterable<String> protocols}) =>
+WebSocketChannel connect(Uri uri, {Iterable<String>? protocols}) =>
     IOWebSocketChannel.connect(uri, protocols: protocols);
diff --git a/lib/src/channel.dart b/lib/src/channel.dart
index 75522a8..fa0cb97 100644
--- a/lib/src/channel.dart
+++ b/lib/src/channel.dart
@@ -35,21 +35,21 @@
   /// For a client socket, this is initially `null`. After the WebSocket
   /// connection is established the value is set to the subprotocol selected by
   /// the server. If no subprotocol is negotiated the value will remain `null`.
-  String get protocol => _webSocket.protocol;
+  String? get protocol => _webSocket.protocol;
 
   /// The [close code][] set when the WebSocket connection is closed.
   ///
   /// [close code]: https://tools.ietf.org/html/rfc6455#section-7.1.5
   ///
   /// Before the connection has been closed, this will be `null`.
-  int get closeCode => _webSocket.closeCode;
+  int? get closeCode => _webSocket.closeCode;
 
   /// The [close reason][] set when the WebSocket connection is closed.
   ///
   /// [close reason]: https://tools.ietf.org/html/rfc6455#section-7.1.6
   ///
   /// Before the connection has been closed, this will be `null`.
-  String get closeReason => _webSocket.closeReason;
+  String? get closeReason => _webSocket.closeReason;
 
   @override
   Stream get stream => StreamView(_webSocket);
@@ -96,7 +96,7 @@
   ///
   /// [WebSocket handshake]: https://tools.ietf.org/html/rfc6455#section-4
   WebSocketChannel(StreamChannel<List<int>> channel,
-      {String protocol, Duration pingInterval, bool serverSide = true})
+      {String? protocol, Duration? pingInterval, bool serverSide = true})
       : _webSocket = WebSocketImpl.fromSocket(
             channel.stream, channel.sink, protocol, serverSide)
           ..pingInterval = pingInterval;
@@ -107,7 +107,7 @@
   /// communicate over the resulting socket.
   ///
   /// The optional [protocols] parameter is the same as `WebSocket.connect`.
-  factory WebSocketChannel.connect(Uri uri, {Iterable<String> protocols}) =>
+  factory WebSocketChannel.connect(Uri uri, {Iterable<String>? protocols}) =>
       platform.connect(uri, protocols: protocols);
 }
 
@@ -131,6 +131,6 @@
   /// [close code]: https://tools.ietf.org/html/rfc6455#section-7.1.5
   /// [reason]: https://tools.ietf.org/html/rfc6455#section-7.1.6
   @override
-  Future close([int closeCode, String closeReason]) =>
+  Future close([int? closeCode, String? closeReason]) =>
       _webSocket.close(closeCode, closeReason);
 }
diff --git a/lib/src/copy/io_sink.dart b/lib/src/copy/io_sink.dart
index 0ee0508..2abf999 100644
--- a/lib/src/copy/io_sink.dart
+++ b/lib/src/copy/io_sink.dart
@@ -16,8 +16,8 @@
 class StreamSinkImpl<T> implements StreamSink<T> {
   final StreamConsumer<T> _target;
   final Completer _doneCompleter = Completer();
-  StreamController<T> _controllerInstance;
-  Completer _controllerCompleter;
+  StreamController<T>? _controllerInstance;
+  Completer? _controllerCompleter;
   bool _isClosed = false;
   bool _isBound = false;
   bool _hasError = false;
@@ -36,7 +36,7 @@
   }
 
   @override
-  void addError(error, [StackTrace stackTrace]) {
+  void addError(error, [StackTrace? stackTrace]) {
     if (_isClosed) {
       return;
     }
@@ -53,7 +53,7 @@
     _isBound = true;
     final future = _controllerCompleter == null
         ? _target.addStream(stream)
-        : _controllerCompleter.future.then((_) => _target.addStream(stream));
+        : _controllerCompleter!.future.then((_) => _target.addStream(stream));
     _controllerInstance?.close();
 
     // Wait for any pending events in [_controller] to be dispatched before
@@ -71,8 +71,8 @@
     // Adding an empty stream-controller will return a future that will complete
     // when all data is done.
     _isBound = true;
-    final future = _controllerCompleter.future;
-    _controllerInstance.close();
+    final future = _controllerCompleter!.future;
+    _controllerInstance!.close();
     return future.whenComplete(() {
       _isBound = false;
     });
@@ -86,7 +86,7 @@
     if (!_isClosed) {
       _isClosed = true;
       if (_controllerInstance != null) {
-        _controllerInstance.close();
+        _controllerInstance!.close();
       } else {
         _closeTarget();
       }
@@ -107,7 +107,7 @@
     }
   }
 
-  void _completeDoneError(error, StackTrace stackTrace) {
+  void _completeDoneError(Object error, StackTrace stackTrace) {
     if (!_doneCompleter.isCompleted) {
       _hasError = true;
       _doneCompleter.completeError(error, stackTrace);
@@ -127,17 +127,17 @@
       _target.addStream(_controller.stream).then((_) {
         if (_isBound) {
           // A new stream takes over - forward values to that stream.
-          _controllerCompleter.complete(this);
+          _controllerCompleter!.complete(this);
           _controllerCompleter = null;
           _controllerInstance = null;
         } else {
           // No new stream, .close was called. Close _target.
           _closeTarget();
         }
-      }, onError: (error, StackTrace stackTrace) {
+      }, onError: (Object error, StackTrace stackTrace) {
         if (_isBound) {
           // A new stream takes over - forward errors to that stream.
-          _controllerCompleter.completeError(error, stackTrace);
+          _controllerCompleter!.completeError(error, stackTrace);
           _controllerCompleter = null;
           _controllerInstance = null;
         } else {
@@ -147,6 +147,6 @@
         }
       });
     }
-    return _controllerInstance;
+    return _controllerInstance!;
   }
 }
diff --git a/lib/src/copy/web_socket_impl.dart b/lib/src/copy/web_socket_impl.dart
index 14cc542..7c35134 100644
--- a/lib/src/copy/web_socket_impl.dart
+++ b/lib/src/copy/web_socket_impl.dart
@@ -89,7 +89,7 @@
   int closeCode = WebSocketStatus.NO_STATUS_RECEIVED;
   String closeReason = '';
 
-  EventSink<dynamic /*List<int>|_WebSocketPing|_WebSocketPong*/ > _eventSink;
+  EventSink<dynamic /*List<int>|_WebSocketPing|_WebSocketPong*/ >? _eventSink;
 
   final bool _serverSide;
   final List<int> _maskingBytes = List.filled(4, 0);
@@ -109,13 +109,13 @@
       });
 
   @override
-  void addError(Object error, [StackTrace stackTrace]) {
-    _eventSink.addError(error, stackTrace);
+  void addError(Object error, [StackTrace? stackTrace]) {
+    _eventSink!.addError(error, stackTrace);
   }
 
   @override
   void close() {
-    _eventSink.close();
+    _eventSink!.close();
   }
 
   /// Process data received from the underlying communication channel.
@@ -292,13 +292,13 @@
         switch (_opcode) {
           case _WebSocketOpcode.CLOSE:
             _state = CLOSED;
-            _eventSink.close();
+            _eventSink!.close();
             break;
           case _WebSocketOpcode.PING:
-            _eventSink.add(_WebSocketPing());
+            _eventSink!.add(_WebSocketPing());
             break;
           case _WebSocketOpcode.PONG:
-            _eventSink.add(_WebSocketPong());
+            _eventSink!.add(_WebSocketPong());
             break;
         }
         _prepareForNextFrame();
@@ -316,10 +316,10 @@
 
       switch (_currentMessageType) {
         case _WebSocketMessageType.TEXT:
-          _eventSink.add(utf8.decode(bytes));
+          _eventSink!.add(utf8.decode(bytes));
           break;
         case _WebSocketMessageType.BINARY:
-          _eventSink.add(bytes);
+          _eventSink!.add(bytes);
           break;
       }
       _currentMessageType = _WebSocketMessageType.NONE;
@@ -345,15 +345,15 @@
           }
         }
         _state = CLOSED;
-        _eventSink.close();
+        _eventSink!.close();
         break;
 
       case _WebSocketOpcode.PING:
-        _eventSink.add(_WebSocketPing(_payload.takeBytes()));
+        _eventSink!.add(_WebSocketPing(_payload.takeBytes()));
         break;
 
       case _WebSocketOpcode.PONG:
-        _eventSink.add(_WebSocketPong(_payload.takeBytes()));
+        _eventSink!.add(_WebSocketPong(_payload.takeBytes()));
         break;
     }
     _prepareForNextFrame();
@@ -377,13 +377,13 @@
 }
 
 class _WebSocketPing {
-  final List<int> payload;
+  final List<int>? payload;
 
   _WebSocketPing([this.payload]);
 }
 
 class _WebSocketPong {
-  final List<int> payload;
+  final List<int>? payload;
 
   _WebSocketPong([this.payload]);
 }
@@ -392,7 +392,7 @@
 class _WebSocketOutgoingTransformer
     extends StreamTransformerBase<dynamic, List<int>> implements EventSink {
   final WebSocketImpl webSocket;
-  EventSink<List<int>> _eventSink;
+  EventSink<List<int>>? _eventSink;
 
   _WebSocketOutgoingTransformer(this.webSocket);
 
@@ -417,7 +417,7 @@
       addFrame(_WebSocketOpcode.PING, message.payload);
       return;
     }
-    List<int> data;
+    List<int>? data;
     int opcode;
     if (message != null) {
       if (message is String) {
@@ -436,15 +436,15 @@
   }
 
   @override
-  void addError(Object error, [StackTrace stackTrace]) {
-    _eventSink.addError(error, stackTrace);
+  void addError(Object error, [StackTrace? stackTrace]) {
+    _eventSink!.addError(error, stackTrace);
   }
 
   @override
   void close() {
     final code = webSocket._outCloseCode;
     final reason = webSocket._outCloseReason;
-    List<int> data;
+    List<int>? data;
     if (code != null) {
       data = <int>[];
       data.add((code >> 8) & 0xFF);
@@ -454,10 +454,10 @@
       }
     }
     addFrame(_WebSocketOpcode.CLOSE, data);
-    _eventSink.close();
+    _eventSink!.close();
   }
 
-  void addFrame(int opcode, List<int> data) {
+  void addFrame(int opcode, List<int>? data) {
     createFrame(
             opcode,
             data,
@@ -466,12 +466,12 @@
             // never be a deflate helper for a cross-platform WebSocket client.
             false)
         .forEach((e) {
-      _eventSink.add(e);
+      _eventSink!.add(e);
     });
   }
 
   static Iterable<List<int>> createFrame(
-      int opcode, List<int> data, bool serverSide, bool compressed) {
+      int opcode, List<int>? data, bool serverSide, bool compressed) {
     final mask = !serverSide; // Masking not implemented for server.
     final dataLength = data == null ? 0 : data.length;
     // Determine the header size.
@@ -566,24 +566,26 @@
 class _WebSocketConsumer implements StreamConsumer {
   final WebSocketImpl webSocket;
   final StreamSink<List<int>> sink;
-  StreamController _controller;
-  StreamSubscription _subscription;
+  StreamController? _controller;
+
+  // ignore: cancel_subscriptions
+  StreamSubscription? _subscription;
   bool _issuedPause = false;
   bool _closed = false;
   final Completer _closeCompleter = Completer<WebSocketImpl>();
-  Completer _completer;
+  Completer<WebSocketImpl>? _completer;
 
   _WebSocketConsumer(this.webSocket, this.sink);
 
   void _onListen() {
     if (_subscription != null) {
-      _subscription.cancel();
+      _subscription!.cancel();
     }
   }
 
   void _onPause() {
     if (_subscription != null) {
-      _subscription.pause();
+      _subscription!.pause();
     } else {
       _issuedPause = true;
     }
@@ -591,7 +593,7 @@
 
   void _onResume() {
     if (_subscription != null) {
-      _subscription.resume();
+      _subscription!.resume();
     } else {
       _issuedPause = false;
     }
@@ -601,7 +603,7 @@
     if (_subscription != null) {
       final subscription = _subscription;
       _subscription = null;
-      subscription.cancel();
+      subscription!.cancel();
     }
   }
 
@@ -613,7 +615,7 @@
         onResume: _onResume,
         onCancel: _onListen);
     final stream =
-        _WebSocketOutgoingTransformer(webSocket).bind(_controller.stream);
+        _WebSocketOutgoingTransformer(webSocket).bind(_controller!.stream);
     sink.addStream(stream).then((_) {
       _done();
       _closeCompleter.complete(webSocket);
@@ -631,12 +633,12 @@
     });
   }
 
-  bool _done([error, StackTrace stackTrace]) {
+  bool _done([Object? error, StackTrace? stackTrace]) {
     if (_completer == null) return false;
     if (error != null) {
-      _completer.completeError(error, stackTrace);
+      _completer!.completeError(error, stackTrace);
     } else {
-      _completer.complete(webSocket);
+      _completer!.complete(webSocket);
     }
     _completer = null;
     return true;
@@ -651,13 +653,13 @@
     _ensureController();
     _completer = Completer();
     _subscription = stream.listen((data) {
-      _controller.add(data);
+      _controller!.add(data);
     }, onDone: _done, onError: _done, cancelOnError: true);
     if (_issuedPause) {
-      _subscription.pause();
+      _subscription!.pause();
       _issuedPause = false;
     }
-    return _completer.future;
+    return _completer!.future;
   }
 
   @override
@@ -666,14 +668,14 @@
     Future closeSocket() =>
         sink.close().catchError((_) {}).then((_) => webSocket);
 
-    _controller.close();
+    _controller!.close();
     return _closeCompleter.future.then((_) => closeSocket());
   }
 
   void add(data) {
     if (_closed) return;
     _ensureController();
-    _controller.add(data);
+    _controller!.add(data);
   }
 
   void closeSocket() {
@@ -689,24 +691,26 @@
   static const int DEFAULT_WINDOW_BITS = 15;
   static const String PER_MESSAGE_DEFLATE = 'permessage-deflate';
 
-  final String protocol;
+  final String? protocol;
 
-  StreamController _controller;
-  StreamSubscription _subscription;
-  StreamSink _sink;
+  late final StreamController _controller;
+
+  // ignore: cancel_subscriptions
+  StreamSubscription? _subscription;
+  late final StreamSink _sink;
 
   final bool _serverSide;
   int _readyState = WebSocket.CONNECTING;
   bool _writeClosed = false;
-  int _closeCode;
-  String _closeReason;
-  Duration _pingInterval;
-  Timer _pingTimer;
-  _WebSocketConsumer _consumer;
+  int? _closeCode;
+  String? _closeReason;
+  Duration? _pingInterval;
+  Timer? _pingTimer;
+  late final _WebSocketConsumer _consumer;
 
-  int _outCloseCode;
-  String _outCloseReason;
-  Timer _closeTimer;
+  int? _outCloseCode;
+  String? _outCloseReason;
+  Timer? _closeTimer;
 
   WebSocketImpl.fromSocket(
       Stream<List<int>> stream, StreamSink<List<int>> sink, this.protocol,
@@ -726,7 +730,7 @@
         _controller.add(data);
       }
     }, onError: (error, stackTrace) {
-      if (_closeTimer != null) _closeTimer.cancel();
+      if (_closeTimer != null) _closeTimer!.cancel();
       if (error is FormatException) {
         _close(WebSocketStatus.INVALID_FRAME_PAYLOAD_DATA);
       } else {
@@ -737,7 +741,7 @@
       _closeReason = _outCloseReason;
       _controller.close();
     }, onDone: () {
-      if (_closeTimer != null) _closeTimer.cancel();
+      if (_closeTimer != null) _closeTimer!.cancel();
       if (_readyState == WebSocket.OPEN) {
         _readyState = WebSocket.CLOSING;
         if (!_isReservedStatusCode(transformer.closeCode)) {
@@ -752,39 +756,39 @@
       _closeReason = transformer.closeReason;
       _controller.close();
     }, cancelOnError: true);
-    _subscription.pause();
+    _subscription!.pause();
     _controller = StreamController(
         sync: true,
-        onListen: () => _subscription.resume(),
+        onListen: () => _subscription!.resume(),
         onCancel: () {
-          _subscription.cancel();
+          _subscription!.cancel();
           _subscription = null;
         },
-        onPause: _subscription.pause,
-        onResume: _subscription.resume);
+        onPause: _subscription!.pause,
+        onResume: _subscription!.resume);
 
     _webSockets[_serviceId] = this;
   }
 
   @override
-  StreamSubscription listen(void Function(dynamic) onData,
-          {Function onError, void Function() onDone, bool cancelOnError}) =>
+  StreamSubscription listen(void Function(dynamic)? onData,
+          {Function? onError, void Function()? onDone, bool? cancelOnError}) =>
       _controller.stream.listen(onData,
           onError: onError, onDone: onDone, cancelOnError: cancelOnError);
 
-  Duration get pingInterval => _pingInterval;
+  Duration? get pingInterval => _pingInterval;
 
-  set pingInterval(Duration interval) {
+  set pingInterval(Duration? interval) {
     if (_writeClosed) return;
-    if (_pingTimer != null) _pingTimer.cancel();
+    if (_pingTimer != null) _pingTimer!.cancel();
     _pingInterval = interval;
 
     if (_pingInterval == null) return;
 
-    _pingTimer = Timer(_pingInterval, () {
+    _pingTimer = Timer(_pingInterval!, () {
       if (_writeClosed) return;
       _consumer.add(_WebSocketPing());
-      _pingTimer = Timer(_pingInterval, () {
+      _pingTimer = Timer(_pingInterval!, () {
         // No pong received.
         _close(WebSocketStatus.GOING_AWAY);
       });
@@ -793,11 +797,11 @@
 
   int get readyState => _readyState;
 
-  String get extensions => null;
+  String? get extensions => null;
 
-  int get closeCode => _closeCode;
+  int? get closeCode => _closeCode;
 
-  String get closeReason => _closeReason;
+  String? get closeReason => _closeReason;
 
   @override
   void add(data) {
@@ -805,7 +809,7 @@
   }
 
   @override
-  void addError(error, [StackTrace stackTrace]) {
+  void addError(Object error, [StackTrace? stackTrace]) {
     _sink.addError(error, stackTrace);
   }
 
@@ -816,7 +820,7 @@
   Future get done => _sink.done;
 
   @override
-  Future close([int code, String reason]) {
+  Future close([int? code, String? reason]) {
     if (_isReservedStatusCode(code)) {
       throw WebSocketChannelException('Reserved status code $code');
     }
@@ -838,7 +842,7 @@
         // Reuse code and reason from the local close.
         _closeCode = _outCloseCode;
         _closeReason = _outCloseReason;
-        if (_subscription != null) _subscription.cancel();
+        if (_subscription != null) _subscription!.cancel();
         _controller.close();
         _webSockets.remove(_serviceId);
       });
@@ -846,7 +850,7 @@
     return _sink.close();
   }
 
-  void _close([int code, String reason]) {
+  void _close([int? code, String? reason]) {
     if (_writeClosed) return;
     if (_outCloseCode == null) {
       _outCloseCode = code;
@@ -861,7 +865,7 @@
   // deleted for web_socket_channel. The methods were unused in WebSocket code
   // and produced warnings.
 
-  static bool _isReservedStatusCode(int code) =>
+  static bool _isReservedStatusCode(int? code) =>
       code != null &&
       (code < WebSocketStatus.NORMAL_CLOSURE ||
           code == WebSocketStatus.RESERVED_1004 ||
diff --git a/lib/src/exception.dart b/lib/src/exception.dart
index ece5cfa..11de8ab 100644
--- a/lib/src/exception.dart
+++ b/lib/src/exception.dart
@@ -6,10 +6,10 @@
 
 /// An exception thrown by a [WebSocketChannel].
 class WebSocketChannelException implements Exception {
-  final String message;
+  final String? message;
 
   /// The exception that caused this one, if available.
-  final Object inner;
+  final Object? inner;
 
   WebSocketChannelException([this.message]) : inner = null;
 
diff --git a/lib/src/sink_completer.dart b/lib/src/sink_completer.dart
index 4d6a2d3..9012d68 100644
--- a/lib/src/sink_completer.dart
+++ b/lib/src/sink_completer.dart
@@ -49,24 +49,24 @@
   ///
   /// Created if the user adds events to this sink before the destination sink
   /// is set.
-  StreamController _controller;
+  StreamController? _controller;
 
   /// Completer for [done].
   ///
   /// Created if the user requests the [done] future before the destination sink
   /// is set.
-  Completer _doneCompleter;
+  Completer? _doneCompleter;
 
   /// Destination sink for the events added to this sink.
   ///
   /// Set when [WebSocketSinkCompleter.setDestinationSink] is called.
-  WebSocketSink _destinationSink;
+  WebSocketSink? _destinationSink;
 
   /// The close code passed to [close].
-  int _closeCode;
+  int? _closeCode;
 
   /// The close reason passed to [close].
-  String _closeReason;
+  String? _closeReason;
 
   /// Whether events should be sent directly to [_destinationSink], as opposed
   /// to going through [_controller].
@@ -74,59 +74,55 @@
 
   @override
   Future get done {
-    if (_doneCompleter != null) return _doneCompleter.future;
+    if (_doneCompleter != null) return _doneCompleter!.future;
     if (_destinationSink == null) {
       _doneCompleter = Completer.sync();
-      return _doneCompleter.future;
+      return _doneCompleter!.future;
     }
-    return _destinationSink.done;
+    return _destinationSink!.done;
   }
 
   @override
   void add(event) {
     if (_canSendDirectly) {
-      _destinationSink.add(event);
+      _destinationSink!.add(event);
     } else {
-      _ensureController();
-      _controller.add(event);
+      _ensureController().add(event);
     }
   }
 
   @override
-  void addError(error, [StackTrace stackTrace]) {
+  void addError(Object error, [StackTrace? stackTrace]) {
     if (_canSendDirectly) {
-      _destinationSink.addError(error, stackTrace);
+      _destinationSink!.addError(error, stackTrace);
     } else {
-      _ensureController();
-      _controller.addError(error, stackTrace);
+      _ensureController().addError(error, stackTrace);
     }
   }
 
   @override
   Future addStream(Stream stream) {
-    if (_canSendDirectly) return _destinationSink.addStream(stream);
+    if (_canSendDirectly) return _destinationSink!.addStream(stream);
 
-    _ensureController();
-    return _controller.addStream(stream, cancelOnError: false);
+    final controller = _ensureController();
+    return controller.addStream(stream, cancelOnError: false);
   }
 
   @override
-  Future close([int closeCode, String closeReason]) {
+  Future close([int? closeCode, String? closeReason]) {
     if (_canSendDirectly) {
-      _destinationSink.close(closeCode, closeReason);
+      _destinationSink!.close(closeCode, closeReason);
     } else {
       _closeCode = closeCode;
       _closeReason = closeReason;
-      _ensureController();
-      _controller.close();
+      _ensureController().close();
     }
     return done;
   }
 
   /// Create [_controller] if it doesn't yet exist.
-  void _ensureController() {
-    _controller ??= StreamController(sync: true);
-  }
+  StreamController _ensureController() =>
+      _controller ??= StreamController(sync: true);
 
   /// Sets the destination sink to which events from this sink will be provided.
   ///
@@ -144,7 +140,7 @@
       // Catch any error that may come from [addStream] or [sink.close]. They'll
       // be reported through [done] anyway.
       sink
-          .addStream(_controller.stream)
+          .addStream(_controller!.stream)
           .whenComplete(() => sink.close(_closeCode, _closeReason))
           .catchError((_) {});
     }
@@ -152,7 +148,7 @@
     // If the user has already asked when the sink is done, connect the sink's
     // done callback to that completer.
     if (_doneCompleter != null) {
-      _doneCompleter.complete(sink.done);
+      _doneCompleter!.complete(sink.done);
     }
   }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 203e709..8b8339a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,20 +1,25 @@
 name: web_socket_channel
-version: 1.2.0
+version: 2.0.0-dev
 
 description: >-
   StreamChannel wrappers for WebSockets. Provides a cross-platform
   WebSocketChannel API, a cross-platform implementation of that API that
   communicates over an underlying StreamChannel.
-homepage: https://github.com/dart-lang/web_socket_channel
+repository: https://github.com/dart-lang/web_socket_channel
 
 environment:
-  sdk: ">=2.10.0 <3.0.0"
+  sdk: ">=2.12.0-0 <3.0.0"
 
 dependencies:
-  async: ">=1.3.0 <3.0.0"
-  crypto: ">=0.9.2 <4.0.0"
-  stream_channel: ">=1.2.0 <3.0.0"
+  async: ^2.5.0
+  crypto: ^3.0.0
+  stream_channel: ^2.1.0
 
 dev_dependencies:
-  pedantic: ^1.0.0
-  test: ^1.16.0-nullsafety
+  pedantic: ^1.10.0
+  test: ^1.16.0
+
+dependency_overrides:
+  # Need to update dependencies on these to allow v2 of this package
+  shelf_web_socket: ^0.2.4
+  test: ^1.16.0
diff --git a/test/html_test.dart b/test/html_test.dart
index aa87c36..3a89098 100644
--- a/test/html_test.dart
+++ b/test/html_test.dart
@@ -3,19 +3,17 @@
 // BSD-style license that can be found in the LICENSE file.
 
 @TestOn('browser')
-
 import 'dart:async';
 import 'dart:html';
 import 'dart:typed_data';
 
 import 'package:async/async.dart';
 import 'package:test/test.dart';
-
 import 'package:web_socket_channel/html.dart';
 import 'package:web_socket_channel/web_socket_channel.dart';
 
 void main() {
-  int port;
+  late int port;
   setUpAll(() async {
     final channel = spawnHybridCode(r'''
       // @dart=2.7
@@ -37,14 +35,10 @@
     port = await channel.stream.first as int;
   });
 
-  WebSocketChannel channel;
-  tearDown(() {
-    if (channel != null) channel.sink.close();
-  });
-
   test('communicates using an existing WebSocket', () async {
     final webSocket = WebSocket('ws://localhost:$port');
-    channel = HtmlWebSocketChannel(webSocket);
+    final channel = HtmlWebSocketChannel(webSocket);
+    addTearDown(channel.sink.close);
 
     final queue = StreamQueue(channel.stream);
     channel.sink.add('foo');
@@ -65,7 +59,8 @@
     final webSocket = WebSocket('ws://localhost:$port');
     await webSocket.onOpen.first;
 
-    channel = HtmlWebSocketChannel(webSocket);
+    final channel = HtmlWebSocketChannel(webSocket);
+    addTearDown(channel.sink.close);
 
     final queue = StreamQueue(channel.stream);
     channel.sink.add('foo');
@@ -73,7 +68,8 @@
   });
 
   test('.connect defaults to binary lists', () async {
-    channel = HtmlWebSocketChannel.connect('ws://localhost:$port');
+    final channel = HtmlWebSocketChannel.connect('ws://localhost:$port');
+    addTearDown(channel.sink.close);
 
     final queue = StreamQueue(channel.stream);
     channel.sink.add('foo');
@@ -85,7 +81,8 @@
 
   test('.connect defaults to binary lists using platform independent api',
       () async {
-    channel = WebSocketChannel.connect(Uri.parse('ws://localhost:$port'));
+    final channel = WebSocketChannel.connect(Uri.parse('ws://localhost:$port'));
+    addTearDown(channel.sink.close);
 
     final queue = StreamQueue(channel.stream);
     channel.sink.add('foo');
@@ -96,8 +93,9 @@
   });
 
   test('.connect can use blobs', () async {
-    channel = HtmlWebSocketChannel.connect('ws://localhost:$port',
+    final channel = HtmlWebSocketChannel.connect('ws://localhost:$port',
         binaryType: BinaryType.blob);
+    addTearDown(channel.sink.close);
 
     final queue = StreamQueue(channel.stream);
     channel.sink.add('foo');
diff --git a/test/io_test.dart b/test/io_test.dart
index bb4a017..111abb4 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -11,12 +11,10 @@
 
 void main() {
   HttpServer server;
-  tearDown(() async {
-    if (server != null) await server.close();
-  });
 
   test('communicates using existing WebSockets', () async {
     server = await HttpServer.bind('localhost', 0);
+    addTearDown(server.close);
     server.transform(WebSocketTransformer()).listen((WebSocket webSocket) {
       final channel = IOWebSocketChannel(webSocket);
       channel.sink.add('hello!');
diff --git a/test/web_socket_test.dart b/test/web_socket_test.dart
index f9873e0..441c628 100644
--- a/test/web_socket_test.dart
+++ b/test/web_socket_test.dart
@@ -65,7 +65,7 @@
           ..set(
               'Sec-WebSocket-Accept',
               WebSocketChannel.signKey(
-                  request.headers.value('Sec-WebSocket-Key')));
+                  request.headers.value('Sec-WebSocket-Key')!));
         response.contentLength = 0;
 
         final socket = await response.detachSocket();