diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 0000000..edbfd65
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,9 @@
+# Dependabot configuration file - enable regular dependencies checks.
+version: 2
+enable-beta-ecosystems: true
+
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..a6d5ca6
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,35 @@
+name: Dart
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v3
+    - uses: dart-lang/setup-dart@v1.3
+    - uses: nanasess/setup-chromedriver@v1.0.7
+
+    - name: Install dependencies
+      run:  dart pub get
+
+    - name: Validate formatting
+      run:  dart format --output=none --set-exit-if-changed .
+
+    - name: Analyze source code
+      run:  dart analyze
+
+    # Disabled; tracked via #75.
+    # - name: Run tests
+    #   run: |
+    #     export DISPLAY=:99
+    #     chromedriver --port=4444 --url-base=/wd/hub &
+    #     sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
+    #     dart test
+    #   env:
+    #     CHROMEDRIVER_ARGS: '--no-sandbox --headless'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 0dfdf99..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-language: dart
-
-dart:
-  - dev
-
-addons:
-  chrome: stable
-
-services:
-  - xvfb
-
-script: ./tool/travis.sh
-
-# Only building master means that we don't run two builds for each pull request.
-branches:
-  only:
-    - master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e05301b..c175eda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,18 @@
-# webkit_inspection_protocol.dart
+## 1.1.0-dev
+
+- Have `ChromeConnection.getTabs` return better exceptions where there's a
+  failure setting up the Chrome connection (#85).
+- Introduce a new, optional `retryFor` parameter to `ChromeConnection.getTabs`.
+  This will re-try failed connections for a period of time; it can be useful to
+  mitigate some intermittent connection issues very early in Chrome's startup.
+
+## 1.0.1
+- Use `package:lints` for analysis.
+- Populate the pubspec `repository` field.
+- Enable the `avoid_dynamic_calls` lint.
+
+## 1.0.0
+- Migrate to null safety.
 
 ## 0.7.5
 - Allow the latest `logging` package.
diff --git a/README.md b/README.md
index ff209a4..0d381ec 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,6 @@
-# webkit_inspection_protocol.dart
-
-[![Build Status](https://travis-ci.org/google/webkit_inspection_protocol.dart.svg)](https://travis-ci.org/google/webkit_inspection_protocol.dart)
+[![Dart](https://github.com/google/webkit_inspection_protocol.dart/actions/workflows/build.yaml/badge.svg)](https://github.com/google/webkit_inspection_protocol.dart/actions/workflows/build.yaml)
 [![pub package](https://img.shields.io/pub/v/webkit_inspection_protocol.svg)](https://pub.dartlang.org/packages/webkit_inspection_protocol)
-
-## What is it?
+[![package publisher](https://img.shields.io/pub/publisher/webkit_inspection_protocol.svg)](https://pub.dev/packages/webkit_inspection_protocol/publisher)
 
 The `webkit_inspection_protocol` package is a client for the Chrome DevTools Protocol
 (previously called the Webkit Inspection Protocol). It's used to talk to Chrome DevTools
diff --git a/analysis_options.yaml b/analysis_options.yaml
index ab3a9b3..8051c3b 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,16 +1,9 @@
+include: package:lints/recommended.yaml
+
 analyzer:
   errors:
     deprecated_member_use_from_same_package: ignore
 
 linter:
   rules:
-    - always_declare_return_types
-    - avoid_init_to_null
-    - directives_ordering
-    - slash_for_doc_comments
-    - prefer_const_constructors
-    - prefer_const_constructors_in_immutables
-    - prefer_const_declarations
-    - prefer_const_literals_to_create_immutables
-    - prefer_final_fields
-    - type_annotate_public_apis
+    - avoid_dynamic_calls
diff --git a/example/multiplex.dart b/example/multiplex.dart
index 6ed123d..8fc6e97 100644
--- a/example/multiplex.dart
+++ b/example/multiplex.dart
@@ -14,7 +14,7 @@
 import 'multiplex_impl.dart' show Server;
 
 void main(List<String> argv) async {
-  var args = (new ArgParser()
+  var args = (ArgParser()
         ..addFlag('verbose', abbr: 'v', defaultsTo: false, negatable: false)
         ..addFlag('model_dom', defaultsTo: false, negatable: true)
         ..addOption('chrome_host', defaultsTo: 'localhost')
@@ -34,8 +34,8 @@
     stderr.writeln('${rec.level.name}: ${rec.time}: ${rec.message}');
   });
 
-  var cr = new ChromeConnection(
+  var cr = ChromeConnection(
       args['chrome_host'] as String, int.parse(args['chrome_port'] as String));
-  new Server(int.parse(args['listen_port'] as String), cr,
+  Server(int.parse(args['listen_port'] as String), cr,
       modelDom: args['model_dom'] as bool);
 }
diff --git a/example/multiplex_impl.dart b/example/multiplex_impl.dart
index c2e5d5a..c8eeb34 100644
--- a/example/multiplex_impl.dart
+++ b/example/multiplex_impl.dart
@@ -19,9 +19,9 @@
     show ChromeConnection, ChromeTab, WipConnection;
 
 class Server {
-  static final _log = new Logger('Server');
+  static final _log = Logger('Server');
 
-  Future<HttpServer> _server;
+  Future<HttpServer>? _server;
   final ChromeConnection chrome;
   final int port;
   final bool modelDom;
@@ -29,13 +29,13 @@
   final _connections = <String, Future<WipConnection>>{};
   final _modelDoms = <String, WipDomModel>{};
 
-  Server(this.port, this.chrome, {this.modelDom}) {
+  Server(this.port, this.chrome, {this.modelDom = false}) {
     _server = io.serve(_handler, InternetAddress.anyIPv4, port);
   }
 
   shelf.Handler get _handler => const shelf.Pipeline()
       .addMiddleware(shelf.logRequests(logger: _shelfLogger))
-      .addHandler(new shelf.Cascade()
+      .addHandler(shelf.Cascade()
           .add(_webSocket)
           .add(_mainPage)
           .add(_json)
@@ -55,14 +55,13 @@
     if (path.isEmpty) {
       var resp = await _mainPageHtml();
       _log.info('mainPage: $resp');
-      return new shelf.Response.ok(resp,
-          headers: {'Content-Type': 'text/html'});
+      return shelf.Response.ok(resp, headers: {'Content-Type': 'text/html'});
     }
-    return new shelf.Response.notFound(null);
+    return shelf.Response.notFound(null);
   }
 
   Future<String> _mainPageHtml() async {
-    var html = new StringBuffer(r'''<!DOCTYPE html>
+    var html = StringBuffer(r'''<!DOCTYPE html>
 <html>
 <head>
 <title>Chrome Windows</title>
@@ -81,12 +80,15 @@
         ..write('/devtools/page/')
         ..write(tab.id)
         ..write('">');
-      if (tab.title != null && tab.title.isNotEmpty) {
+      if (tab.title != null && tab.title!.isNotEmpty) {
         html.write(tab.title);
       } else {
         html.write(tab.url);
       }
-      html..write('</a></td><td>')..write(tab.description)..write('</td></tr>');
+      html
+        ..write('</a></td><td>')
+        ..write(tab.description)
+        ..write('</td></tr>');
     }
     html.write(r'''</tbody>
 </table>
@@ -100,10 +102,10 @@
     if (path.length == 1 && path[0] == 'json') {
       var resp = jsonEncode(await chrome.getTabs(), toEncodable: _jsonEncode);
       _log.info('json: $resp');
-      return new shelf.Response.ok(resp,
+      return shelf.Response.ok(resp,
           headers: {'Content-Type': 'application/json'});
     }
-    return new shelf.Response.notFound(null);
+    return shelf.Response.notFound(null);
   }
 
   Future<shelf.Response> _forward(shelf.Request request) async {
@@ -111,33 +113,35 @@
     var dtResp = await chrome.getUrl(request.url.path);
 
     if (dtResp.statusCode == 200) {
-      return new shelf.Response.ok(dtResp,
+      return shelf.Response.ok(dtResp,
           headers: {'Content-Type': dtResp.headers.contentType.toString()});
     }
     _log.warning(
         'Forwarded ${request.url} returned statusCode: ${dtResp.statusCode}');
-    return new shelf.Response.notFound(null);
+    return shelf.Response.notFound(null);
   }
 
   Future<shelf.Response> _webSocket(shelf.Request request) async {
     var path = request.url.pathSegments;
     if (path.length != 3 || path[0] != 'devtools' || path[1] != 'page') {
-      return new shelf.Response.notFound(null);
+      return shelf.Response.notFound(null);
     }
     _log.info('connecting to websocket: ${request.url}');
 
+    // TODO: The first arg of webSocketHandler is untyped; consider refactoring
+    // (in package:web_socket_channel) to use a typedef.
     return ws.webSocketHandler((WebSocketChannel webSocket) async {
       var debugger = await _connections.putIfAbsent(path[2], () async {
-        var tab = await chrome.getTab((tab) => tab.id == path[2]);
+        var tab = (await chrome.getTab((tab) => tab.id == path[2]))!;
         return WipConnection.connect(tab.webSocketDebuggerUrl);
       });
-      WipDomModel dom;
+      WipDomModel? dom;
       if (modelDom) {
-        dom = await _modelDoms.putIfAbsent(path[2], () {
-          return new WipDomModel(debugger.dom);
+        dom = _modelDoms.putIfAbsent(path[2], () {
+          return WipDomModel(debugger.dom);
         });
       }
-      var forwarder = new WipForwarder(debugger, webSocket.stream.cast(),
+      var forwarder = WipForwarder(debugger, webSocket.stream.cast(),
           sink: webSocket.sink, domModel: dom);
       debugger.onClose.listen((_) {
         _connections.remove(path[2]);
@@ -149,12 +153,12 @@
 
   Future close() async {
     if (_server != null) {
-      await (await _server).close(force: true);
+      await (await _server!).close(force: true);
       _server = null;
     }
   }
 
-  Object _jsonEncode(Object obj) {
+  Object? _jsonEncode(Object? obj) {
     if (obj is ChromeTab) {
       var json = <String, dynamic>{
         'description': obj.description,
diff --git a/lib/dom_model.dart b/lib/dom_model.dart
index 72e70da..9bb5f05 100644
--- a/lib/dom_model.dart
+++ b/lib/dom_model.dart
@@ -26,55 +26,55 @@
 /// Implementation of WipDom that maintains and updates a model of the DOM
 /// based on incoming events.
 class WipDomModel implements WipDom {
-  static final _log = new Logger('WipDomModel');
+  static final _log = Logger('WipDomModel');
 
   final WipDom _dom;
 
   final Map<int, _Node> _nodeCache = {};
-  Future<_Node> _root;
+  Future<_Node>? _root;
 
-  Stream<AttributeModifiedEvent> onAttributeModified;
-  Stream<AttributeRemovedEvent> onAttributeRemoved;
-  Stream<CharacterDataModifiedEvent> onCharacterDataModified;
-  Stream<ChildNodeCountUpdatedEvent> onChildNodeCountUpdated;
-  Stream<ChildNodeInsertedEvent> onChildNodeInserted;
-  Stream<ChildNodeRemovedEvent> onChildNodeRemoved;
-  Stream<DocumentUpdatedEvent> onDocumentUpdated;
-  Stream<SetChildNodesEvent> onSetChildNodes;
+  @override
+  late final Stream<AttributeModifiedEvent> onAttributeModified =
+      StreamTransformer.fromHandlers(handleData: _onAttributeModified)
+          .bind(_dom.onAttributeModified);
+  @override
+  late final Stream<AttributeRemovedEvent> onAttributeRemoved =
+      StreamTransformer.fromHandlers(handleData: _onAttributeRemoved)
+          .bind(_dom.onAttributeRemoved);
+  @override
+  late final Stream<CharacterDataModifiedEvent> onCharacterDataModified =
+      StreamTransformer.fromHandlers(handleData: _onCharacterDataModified)
+          .bind(_dom.onCharacterDataModified);
+  @override
+  late final Stream<ChildNodeCountUpdatedEvent> onChildNodeCountUpdated =
+      StreamTransformer.fromHandlers(handleData: _onChildNodeCountUpdated)
+          .bind(_dom.onChildNodeCountUpdated);
+  @override
+  late final Stream<ChildNodeInsertedEvent> onChildNodeInserted =
+      StreamTransformer.fromHandlers(handleData: _onChildNodeInserted)
+          .bind(_dom.onChildNodeInserted);
+  @override
+  late final Stream<ChildNodeRemovedEvent> onChildNodeRemoved =
+      StreamTransformer.fromHandlers(handleData: _onChildNodeRemoved)
+          .bind(_dom.onChildNodeRemoved);
+  @override
+  late final Stream<DocumentUpdatedEvent> onDocumentUpdated =
+      StreamTransformer.fromHandlers(handleData: _onDocumentUpdated)
+          .bind(_dom.onDocumentUpdated);
+  @override
+  late final Stream<SetChildNodesEvent> onSetChildNodes =
+      StreamTransformer.fromHandlers(handleData: _onSetChildNodes)
+          .bind(_dom.onSetChildNodes);
 
   WipDomModel(this._dom) {
-    onAttributeModified =
-        new StreamTransformer.fromHandlers(handleData: _onAttributeModified)
-            .bind(_dom.onAttributeModified)
-              ..listen(_logEvent);
-    onAttributeRemoved =
-        new StreamTransformer.fromHandlers(handleData: _onAttributeRemoved)
-            .bind(_dom.onAttributeRemoved)
-              ..listen(_logEvent);
-    onCharacterDataModified =
-        new StreamTransformer.fromHandlers(handleData: _onCharacterDataModified)
-            .bind(_dom.onCharacterDataModified)
-              ..listen(_logEvent);
-    onChildNodeCountUpdated =
-        new StreamTransformer.fromHandlers(handleData: _onChildNodeCountUpdated)
-            .bind(_dom.onChildNodeCountUpdated)
-              ..listen(_logEvent);
-    onChildNodeInserted =
-        new StreamTransformer.fromHandlers(handleData: _onChildNodeInserted)
-            .bind(_dom.onChildNodeInserted)
-              ..listen(_logEvent);
-    onChildNodeRemoved =
-        new StreamTransformer.fromHandlers(handleData: _onChildNodeRemoved)
-            .bind(_dom.onChildNodeRemoved)
-              ..listen(_logEvent);
-    onDocumentUpdated =
-        new StreamTransformer.fromHandlers(handleData: _onDocumentUpdated)
-            .bind(_dom.onDocumentUpdated)
-              ..listen(_logEvent);
-    onSetChildNodes =
-        new StreamTransformer.fromHandlers(handleData: _onSetChildNodes)
-            .bind(_dom.onSetChildNodes)
-              ..listen(_logEvent);
+    onAttributeModified.listen(_logEvent);
+    onAttributeRemoved.listen(_logEvent);
+    onCharacterDataModified.listen(_logEvent);
+    onChildNodeCountUpdated.listen(_logEvent);
+    onChildNodeInserted.listen(_logEvent);
+    onChildNodeRemoved.listen(_logEvent);
+    onDocumentUpdated.listen(_logEvent);
+    onSetChildNodes.listen(_logEvent);
   }
 
   void _logEvent(WipEvent event) {
@@ -84,14 +84,14 @@
   void _onAttributeModified(
       AttributeModifiedEvent event, EventSink<AttributeModifiedEvent> sink) {
     var node = _getOrCreateNode(event.nodeId);
-    node._attributes[event.name] = event.value;
+    node._attributes![event.name] = event.value;
     sink.add(event);
   }
 
   void _onAttributeRemoved(
       AttributeRemovedEvent event, EventSink<AttributeRemovedEvent> sink) {
     var node = _getOrCreateNode(event.nodeId);
-    node._attributes.remove(event.name);
+    node._attributes!.remove(event.name);
     sink.add(event);
   }
 
@@ -112,14 +112,11 @@
   void _onChildNodeInserted(
       ChildNodeInsertedEvent event, EventSink<ChildNodeInsertedEvent> sink) {
     var parent = _getOrCreateNode(event.parentNodeId);
-    int index = 0;
-    if (event.previousNodeId != null) {
-      index =
-          parent._children.indexOf(_getOrCreateNode(event.previousNodeId)) + 1;
-    }
+    int index =
+        parent._children!.indexOf(_getOrCreateNode(event.previousNodeId)) + 1;
     var node = _getOrCreateNodeFromNode(event.node);
-    parent._children.insert(index, node);
-    parent._childNodeCount = parent._children.length;
+    parent._children!.insert(index, node);
+    parent._childNodeCount = parent._children!.length;
     sink.add(event);
   }
 
@@ -127,8 +124,8 @@
       ChildNodeRemovedEvent event, EventSink<ChildNodeRemovedEvent> sink) {
     var parent = _getOrCreateNode(event.parentNodeId);
     var node = _nodeCache.remove(event.nodeId);
-    parent._children.remove(node);
-    parent._childNodeCount = parent._children.length;
+    parent._children!.remove(node);
+    parent._childNodeCount = parent._children!.length;
     sink.add(event);
   }
 
@@ -144,7 +141,7 @@
     var parent = _getOrCreateNode(event.nodeId);
     parent._children =
         event.nodes.map(_getOrCreateNodeFromNode).toList(growable: true);
-    parent._childNodeCount = parent._children.length;
+    parent._childNodeCount = parent._children!.length;
     sink.add(event);
   }
 
@@ -152,7 +149,7 @@
   Future<Map<String, String>> getAttributes(int nodeId) async {
     Map<String, String> attributes = await _dom.getAttributes(nodeId);
     var node = _getOrCreateNode(nodeId);
-    node._attributes = new Map.from(attributes);
+    node._attributes = Map.from(attributes);
     return attributes;
   }
 
@@ -161,28 +158,26 @@
   /// multiple times on the same page.
   @override
   Future<Node> getDocument() {
-    if (_root == null) {
-      _root = _dom.getDocument().then((n) => _getOrCreateNodeFromNode(n));
-    }
-    return _root;
+    _root ??= _dom.getDocument().then((n) => _getOrCreateNodeFromNode(n));
+    return _root!;
   }
 
   _Node _getOrCreateNode(int nodeId) =>
-      _nodeCache.putIfAbsent(nodeId, () => new _Node(nodeId));
+      _nodeCache.putIfAbsent(nodeId, () => _Node(nodeId));
 
   _Node _getOrCreateNodeFromNode(Node src) {
     try {
       var node = _getOrCreateNode(src.nodeId);
       if (src.attributes != null) {
-        node._attributes = new Map.from(src.attributes);
+        node._attributes = Map.of(src.attributes!);
       }
       if (src.children != null) {
         node._children =
-            src.children.map(_getOrCreateNodeFromNode).toList(growable: true);
+            src.children!.map(_getOrCreateNodeFromNode).toList(growable: true);
       }
-      node._childNodeCount = src.childNodeCount;
+      node._childNodeCount = src.childNodeCount ?? 0;
       if (src.contentDocument != null) {
-        node._contentDocument = _getOrCreateNodeFromNode(src.contentDocument);
+        node._contentDocument = _getOrCreateNodeFromNode(src.contentDocument!);
       }
       node._documentUrl = src.documentUrl;
       node._internalSubset = src.internalSubset;
@@ -202,94 +197,95 @@
     }
   }
 
+  @override
   dynamic noSuchMethod(Invocation invocation) =>
       reflect(_dom).delegate(invocation);
 }
 
 class _Node implements Node {
-  Map<String, String> _attributes;
+  Map<String, String>? _attributes;
 
   @override
-  Map<String, String> get attributes =>
-      _attributes != null ? new UnmodifiableMapView(_attributes) : null;
+  Map<String, String>? get attributes =>
+      _attributes != null ? UnmodifiableMapView(_attributes!) : null;
 
-  int _childNodeCount;
+  int? _childNodeCount;
 
   @override
-  int get childNodeCount => _childNodeCount;
+  int? get childNodeCount => _childNodeCount;
 
-  List<_Node> _children;
+  List<_Node>? _children;
 
   @override
-  List<Node> get children =>
-      _children != null ? new UnmodifiableListView(_children) : null;
+  List<Node>? get children =>
+      _children != null ? UnmodifiableListView(_children!) : null;
 
-  _Node _contentDocument;
+  _Node? _contentDocument;
 
   @override
-  Node get contentDocument => _contentDocument;
+  Node? get contentDocument => _contentDocument;
 
-  String _documentUrl;
+  String? _documentUrl;
 
   @override
-  String get documentUrl => _documentUrl;
+  String? get documentUrl => _documentUrl;
 
-  String _internalSubset;
+  String? _internalSubset;
 
   @override
-  String get internalSubset => _internalSubset;
+  String? get internalSubset => _internalSubset;
 
-  String _localName;
+  String? _localName;
 
   @override
-  String get localName => _localName;
+  String get localName => _localName!;
 
-  String _name;
+  String? _name;
 
   @override
-  String get name => _name;
+  String? get name => _name;
 
   @override
   final int nodeId;
 
-  String _nodeName;
+  String? _nodeName;
 
   @override
-  String get nodeName => _nodeName;
+  String get nodeName => _nodeName!;
 
-  int _nodeType;
+  int? _nodeType;
 
   @override
-  int get nodeType => _nodeType;
+  int get nodeType => _nodeType!;
 
-  String _nodeValue;
+  String? _nodeValue;
 
   @override
-  String get nodeValue => _nodeValue;
+  String get nodeValue => _nodeValue!;
 
-  String _publicId;
+  String? _publicId;
 
   @override
-  String get publicId => _publicId;
+  String? get publicId => _publicId;
 
-  String _systemId;
+  String? _systemId;
 
   @override
-  String get systemId => _systemId;
+  String? get systemId => _systemId;
 
-  String _value;
+  String? _value;
 
   @override
-  String get value => _value;
+  String? get value => _value;
 
-  String _xmlVersion;
+  String? _xmlVersion;
 
   @override
-  String get xmlVersion => _xmlVersion;
+  String? get xmlVersion => _xmlVersion;
 
   _Node(this.nodeId);
 
-  Map toJson() => _toJsonInternal(new Set());
+  Map toJson() => _toJsonInternal({});
 
   Map _toJsonInternal(Set visited) {
     var map = {
@@ -300,44 +296,42 @@
       'nodeValue': nodeValue
     };
     if (visited.add(nodeId)) {
-      if (attributes != null && attributes.isNotEmpty) {
-        map['attributes'] = flattenAttributesMap(attributes);
+      if (attributes != null && attributes!.isNotEmpty) {
+        map['attributes'] = flattenAttributesMap(attributes!);
       }
       if (childNodeCount != null) {
-        map['childNodeCount'] = childNodeCount;
+        map['childNodeCount'] = childNodeCount!;
       }
-      if (_children != null && _children.isNotEmpty) {
+      if (_children != null && _children!.isNotEmpty) {
         var newChildren = [];
-        _children.forEach((child) {
-          if (child != null) {
-            newChildren.add(child._toJsonInternal(visited));
-          }
-        });
+        for (var child in _children!) {
+          newChildren.add(child._toJsonInternal(visited));
+        }
         map['children'] = newChildren;
       }
       if (_contentDocument != null) {
-        map['contentDocument'] = _contentDocument._toJsonInternal(visited);
+        map['contentDocument'] = _contentDocument!._toJsonInternal(visited);
       }
       if (documentUrl != null) {
-        map['documentUrl'] = documentUrl;
+        map['documentUrl'] = documentUrl!;
       }
       if (internalSubset != null) {
-        map['internalSubset'] = internalSubset;
+        map['internalSubset'] = internalSubset!;
       }
       if (name != null) {
-        map['name'] = name;
+        map['name'] = name!;
       }
       if (publicId != null) {
-        map['publicId'] = publicId;
+        map['publicId'] = publicId!;
       }
       if (systemId != null) {
-        map['systemId'] = systemId;
+        map['systemId'] = systemId!;
       }
       if (value != null) {
-        map['value'] = value;
+        map['value'] = value!;
       }
       if (xmlVersion != null) {
-        map['xmlVersion'] = xmlVersion;
+        map['xmlVersion'] = xmlVersion!;
       }
     }
     return map;
@@ -347,9 +341,9 @@
 List<String> flattenAttributesMap(Map<String, String> attributes) {
   var result = <String>[];
   attributes.forEach((k, v) {
-    if (k != null) {
-      result..add(k)..add(v);
-    }
+    result
+      ..add(k)
+      ..add(v);
   });
   return result;
 }
diff --git a/lib/forwarder.dart b/lib/forwarder.dart
index 85f9f47..16a1ac0 100644
--- a/lib/forwarder.dart
+++ b/lib/forwarder.dart
@@ -16,12 +16,12 @@
 /// Forwards a [Stream] to a [WipConnection] and events
 /// from a [WipConnection] to a [StreamSink].
 class WipForwarder {
-  static final _log = new Logger('ChromeForwarder');
+  static final _log = Logger('ChromeForwarder');
 
   final Stream<String> _in;
   final StreamSink _out;
   final WipConnection _debugger;
-  final WipDom domModel;
+  final WipDom? domModel;
 
   /// If false, no Debugger.paused events will be forwarded back to the client.
   /// This gets automatically set to true if a breakpoint is set by the client.
@@ -29,15 +29,12 @@
 
   final List<StreamSubscription> _subscriptions = <StreamSubscription>[];
 
-  final StreamController<Null> _closedController =
-      new StreamController.broadcast();
+  final StreamController<void> _closedController = StreamController.broadcast();
 
   factory WipForwarder(WipConnection debugger, Stream<String> stream,
-      {StreamSink sink, WipDom domModel}) {
-    if (sink == null) {
-      sink = stream as StreamSink;
-    }
-    return new WipForwarder._(debugger, stream, sink, domModel);
+      {StreamSink? sink, WipDom? domModel}) {
+    sink ??= stream as StreamSink;
+    return WipForwarder._(debugger, stream, sink, domModel);
   }
 
   WipForwarder._(this._debugger, this._in, this._out, this.domModel) {
@@ -48,7 +45,7 @@
   }
 
   Future _onClientDataHandler(String data) async {
-    var json = jsonDecode(data);
+    var json = jsonDecode(data) as Map<String, dynamic>;
     var response = {'id': json['id']};
     _log.info('Forwarding to debugger: $data');
     try {
@@ -63,12 +60,12 @@
       if (domModel != null) {
         switch (method) {
           case 'DOM.getDocument':
-            response['result'] = {'root': (await domModel.getDocument())};
+            response['result'] = {'root': (await domModel!.getDocument())};
             processed = true;
             break;
           case 'DOM.getAttributes':
             var attributes = flattenAttributesMap(
-                await domModel.getAttributes(params['nodeId'] as int));
+                await domModel!.getAttributes(params['nodeId'] as int));
             response['result'] = {'attributes': attributes};
             processed = true;
             break;
@@ -125,21 +122,27 @@
   void pause() {
     assert(_subscriptions.isNotEmpty);
     _log.info('Pausing forwarding');
-    _subscriptions.forEach((s) => s.pause());
+    for (var s in _subscriptions) {
+      s.pause();
+    }
     _subscriptions.clear();
   }
 
   void resume() {
     assert(_subscriptions.isNotEmpty);
     _log.info('Resuming forwarding');
-    _subscriptions.forEach((s) => s.resume());
+    for (var s in _subscriptions) {
+      s.resume();
+    }
     _subscriptions.clear();
   }
 
   Future stop() {
     assert(_subscriptions.isNotEmpty);
     _log.info('Stopping forwarding');
-    _subscriptions.forEach((s) => s.cancel());
+    for (var s in _subscriptions) {
+      s.cancel();
+    }
     _subscriptions.clear();
     _closedController.add(null);
     return Future.wait([_closedController.close(), _out.close()]);
diff --git a/lib/src/console.dart b/lib/src/console.dart
index e1500d7..4864a9f 100644
--- a/lib/src/console.dart
+++ b/lib/src/console.dart
@@ -17,33 +17,34 @@
 
   Stream<ConsoleMessageEvent> get onMessage => eventStream(
       'Console.messageAdded',
-      (WipEvent event) => new ConsoleMessageEvent(event.json));
+      (WipEvent event) => ConsoleMessageEvent(event.json));
 
   Stream<ConsoleClearedEvent> get onCleared => eventStream(
       'Console.messagesCleared',
-      (WipEvent event) => new ConsoleClearedEvent(event.json));
+      (WipEvent event) => ConsoleClearedEvent(event.json));
 }
 
 class ConsoleMessageEvent extends WipEvent {
   ConsoleMessageEvent(Map<String, dynamic> json) : super(json);
 
-  Map get _message => params['message'] as Map;
+  Map get _message => params!['message'] as Map;
 
   String get text => _message['text'] as String;
 
   String get level => _message['level'] as String;
 
-  String get url => _message['url'] as String;
+  String? get url => _message['url'] as String?;
 
   Iterable<WipConsoleCallFrame> getStackTrace() {
     if (_message.containsKey('stackTrace')) {
-      return (params['stackTrace'] as List).map((frame) =>
-          new WipConsoleCallFrame.fromMap(frame as Map<String, dynamic>));
+      return (params!['stackTrace'] as List).map((frame) =>
+          WipConsoleCallFrame.fromMap(frame as Map<String, dynamic>));
     } else {
       return [];
     }
   }
 
+  @override
   String toString() => text;
 }
 
diff --git a/lib/src/debugger.dart b/lib/src/debugger.dart
index a98f2c9..73bf2ca 100644
--- a/lib/src/debugger.dart
+++ b/lib/src/debugger.dart
@@ -25,19 +25,19 @@
   Future<String> getScriptSource(String scriptId) async {
     return (await sendCommand('Debugger.getScriptSource',
             params: {'scriptId': scriptId}))
-        .result['scriptSource'] as String;
+        .result!['scriptSource'] as String;
   }
 
   Future<WipResponse> pause() => sendCommand('Debugger.pause');
 
   Future<WipResponse> resume() => sendCommand('Debugger.resume');
 
-  Future<WipResponse> stepInto({Map<String, dynamic> params}) =>
+  Future<WipResponse> stepInto({Map<String, dynamic>? params}) =>
       sendCommand('Debugger.stepInto', params: params);
 
   Future<WipResponse> stepOut() => sendCommand('Debugger.stepOut');
 
-  Future<WipResponse> stepOver({Map<String, dynamic> params}) =>
+  Future<WipResponse> stepOver({Map<String, dynamic>? params}) =>
       sendCommand('Debugger.stepOver', params: params);
 
   Future<WipResponse> setPauseOnExceptions(PauseState state) {
@@ -53,7 +53,7 @@
   ///    evaluates to true.
   Future<SetBreakpointResponse> setBreakpoint(
     WipLocation location, {
-    String condition,
+    String? condition,
   }) async {
     Map<String, dynamic> params = {
       'location': location.toJsonMap(),
@@ -65,11 +65,11 @@
     final WipResponse response =
         await sendCommand('Debugger.setBreakpoint', params: params);
 
-    if (response.result.containsKey('exceptionDetails')) {
-      throw new ExceptionDetails(
-          response.result['exceptionDetails'] as Map<String, dynamic>);
+    if (response.result!.containsKey('exceptionDetails')) {
+      throw ExceptionDetails(
+          response.result!['exceptionDetails'] as Map<String, dynamic>);
     } else {
-      return new SetBreakpointResponse(response.json);
+      return SetBreakpointResponse(response.json);
     }
   }
 
@@ -88,7 +88,7 @@
   Future<RemoteObject> evaluateOnCallFrame(
     String callFrameId,
     String expression, {
-    bool returnByValue,
+    bool? returnByValue,
   }) async {
     Map<String, dynamic> params = {
       'callFrameId': callFrameId,
@@ -101,12 +101,11 @@
     final WipResponse response =
         await sendCommand('Debugger.evaluateOnCallFrame', params: params);
 
-    if (response.result.containsKey('exceptionDetails')) {
-      throw new ExceptionDetails(
-          response.result['exceptionDetails'] as Map<String, dynamic>);
+    if (response.result!.containsKey('exceptionDetails')) {
+      throw ExceptionDetails(
+          response.result!['exceptionDetails'] as Map<String, dynamic>);
     } else {
-      return new RemoteObject(
-          response.result['result'] as Map<String, dynamic>);
+      return RemoteObject(response.result!['result'] as Map<String, dynamic>);
     }
   }
 
@@ -120,8 +119,8 @@
   ///   (non-nested) function as start.
   Future<List<WipBreakLocation>> getPossibleBreakpoints(
     WipLocation start, {
-    WipLocation end,
-    bool restrictToFunction,
+    WipLocation? end,
+    bool? restrictToFunction,
   }) async {
     Map<String, dynamic> params = {
       'start': start.toJsonMap(),
@@ -136,11 +135,11 @@
     final WipResponse response =
         await sendCommand('Debugger.getPossibleBreakpoints', params: params);
 
-    if (response.result.containsKey('exceptionDetails')) {
-      throw new ExceptionDetails(
-          response.result['exceptionDetails'] as Map<String, dynamic>);
+    if (response.result!.containsKey('exceptionDetails')) {
+      throw ExceptionDetails(
+          response.result!['exceptionDetails'] as Map<String, dynamic>);
     } else {
-      List locations = response.result['locations'];
+      List locations = response.result!['locations'];
       return List.from(locations.map((map) => WipBreakLocation(map)));
     }
   }
@@ -155,21 +154,21 @@
     });
   }
 
-  Stream<DebuggerPausedEvent> get onPaused => eventStream('Debugger.paused',
-      (WipEvent event) => new DebuggerPausedEvent(event.json));
+  Stream<DebuggerPausedEvent> get onPaused => eventStream(
+      'Debugger.paused', (WipEvent event) => DebuggerPausedEvent(event.json));
 
   Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared => eventStream(
       'Debugger.globalObjectCleared',
-      (WipEvent event) => new GlobalObjectClearedEvent(event.json));
+      (WipEvent event) => GlobalObjectClearedEvent(event.json));
 
-  Stream<DebuggerResumedEvent> get onResumed => eventStream('Debugger.resumed',
-      (WipEvent event) => new DebuggerResumedEvent(event.json));
+  Stream<DebuggerResumedEvent> get onResumed => eventStream(
+      'Debugger.resumed', (WipEvent event) => DebuggerResumedEvent(event.json));
 
   Stream<ScriptParsedEvent> get onScriptParsed => eventStream(
       'Debugger.scriptParsed',
-      (WipEvent event) => new ScriptParsedEvent(event.json));
+      (WipEvent event) => ScriptParsedEvent(event.json));
 
-  Map<String, WipScript> get scripts => new UnmodifiableMapView(_scripts);
+  Map<String, WipScript> get scripts => UnmodifiableMapView(_scripts);
 }
 
 String _pauseStateToString(PauseState state) {
@@ -181,7 +180,7 @@
     case PauseState.uncaught:
       return 'uncaught';
     default:
-      throw new ArgumentError('unknown state: $state');
+      throw ArgumentError('unknown state: $state');
   }
 }
 
@@ -190,8 +189,9 @@
 class ScriptParsedEvent extends WipEvent {
   ScriptParsedEvent(Map<String, dynamic> json) : super(json);
 
-  WipScript get script => new WipScript(params);
+  WipScript get script => WipScript(params!);
 
+  @override
   String toString() => script.toString();
 }
 
@@ -209,31 +209,32 @@
   DebuggerPausedEvent(Map<String, dynamic> json) : super(json);
 
   /// Call stack the virtual machine stopped on.
-  List<WipCallFrame> getCallFrames() => (params['callFrames'] as List)
-      .map((frame) => new WipCallFrame(frame as Map<String, dynamic>))
+  List<WipCallFrame> getCallFrames() => (params!['callFrames'] as List)
+      .map((frame) => WipCallFrame(frame as Map<String, dynamic>))
       .toList();
 
   /// Pause reason.
   ///
   /// Allowed Values: ambiguous, assert, debugCommand, DOM, EventListener,
   /// exception, instrumentation, OOM, other, promiseRejection, XHR.
-  String get reason => params['reason'] as String;
+  String get reason => params!['reason'] as String;
 
   /// Object containing break-specific auxiliary properties.
-  Object get data => params['data'];
+  Object? get data => params!['data'];
 
   /// Hit breakpoints IDs (optional).
-  List<String> get hitBreakpoints {
-    if (params['hitBreakpoints'] == null) return null;
-    return (params['hitBreakpoints'] as List).cast<String>();
+  List<String>? get hitBreakpoints {
+    if (params!['hitBreakpoints'] == null) return null;
+    return (params!['hitBreakpoints'] as List).cast<String>();
   }
 
   /// Async stack trace, if any.
-  StackTrace get asyncStackTrace => params['asyncStackTrace'] == null
+  StackTrace? get asyncStackTrace => params!['asyncStackTrace'] == null
       ? null
-      : StackTrace(params['asyncStackTrace']);
+      : StackTrace(params!['asyncStackTrace']);
 
-  String toString() => 'paused: ${reason}';
+  @override
+  String toString() => 'paused: $reason';
 }
 
 /// A debugger call frame.
@@ -254,29 +255,30 @@
 
   /// Location in the source code.
   WipLocation get location =>
-      new WipLocation(json['location'] as Map<String, dynamic>);
+      WipLocation(json['location'] as Map<String, dynamic>);
 
   /// JavaScript script name or url.
   String get url => json['url'] as String;
 
   /// Scope chain for this call frame.
   Iterable<WipScope> getScopeChain() => (json['scopeChain'] as List)
-      .map((scope) => new WipScope(scope as Map<String, dynamic>));
+      .map((scope) => WipScope(scope as Map<String, dynamic>));
 
   /// `this` object for this call frame.
   RemoteObject get thisObject =>
-      new RemoteObject(json['this'] as Map<String, dynamic>);
+      RemoteObject(json['this'] as Map<String, dynamic>);
 
   /// The value being returned, if the function is at return point.
   ///
   /// (optional)
-  RemoteObject get returnValue {
+  RemoteObject? get returnValue {
     return json.containsKey('returnValue')
-        ? new RemoteObject(json['returnValue'] as Map<String, dynamic>)
+        ? RemoteObject(json['returnValue'] as Map<String, dynamic>)
         : null;
   }
 
-  String toString() => '[${functionName}]';
+  @override
+  String toString() => '[$functionName]';
 }
 
 class WipLocation {
@@ -284,7 +286,7 @@
 
   WipLocation(this.json);
 
-  WipLocation.fromValues(String scriptId, int lineNumber, {int columnNumber})
+  WipLocation.fromValues(String scriptId, int lineNumber, {int? columnNumber})
       : json = {} {
     json['scriptId'] = scriptId;
     json['lineNumber'] = lineNumber;
@@ -297,13 +299,14 @@
 
   int get lineNumber => json['lineNumber'];
 
-  int get columnNumber => json['columnNumber'];
+  int? get columnNumber => json['columnNumber'];
 
   Map<String, dynamic> toJsonMap() {
     return json;
   }
 
-  String toString() => '[${scriptId}:${lineNumber}:${columnNumber}]';
+  @override
+  String toString() => '[$scriptId:$lineNumber:$columnNumber]';
 }
 
 class WipScript {
@@ -323,11 +326,12 @@
 
   int get endColumn => json['endColumn'] as int;
 
-  bool get isContentScript => json['isContentScript'] as bool;
+  bool? get isContentScript => json['isContentScript'] as bool?;
 
-  String get sourceMapURL => json['sourceMapURL'] as String;
+  String? get sourceMapURL => json['sourceMapURL'] as String?;
 
-  String toString() => '[script ${scriptId}: ${url}]';
+  @override
+  String toString() => '[script $scriptId: $url]';
 }
 
 class WipScope {
@@ -339,20 +343,20 @@
   String get scope => json['type'] as String;
 
   /// Name of the scope, null if unnamed closure or global scope
-  String get name => json['name'] as String;
+  String? get name => json['name'] as String?;
 
   /// Object representing the scope. For global and with scopes it represents
   /// the actual object; for the rest of the scopes, it is artificial transient
   /// object enumerating scope variables as its properties.
   RemoteObject get object =>
-      new RemoteObject(json['object'] as Map<String, dynamic>);
+      RemoteObject(json['object'] as Map<String, dynamic>);
 }
 
 class WipBreakLocation extends WipLocation {
   WipBreakLocation(Map<String, dynamic> json) : super(json);
 
   WipBreakLocation.fromValues(String scriptId, int lineNumber,
-      {int columnNumber, String type})
+      {int? columnNumber, String? type})
       : super.fromValues(scriptId, lineNumber, columnNumber: columnNumber) {
     if (type != null) {
       json['type'] = type;
@@ -360,14 +364,14 @@
   }
 
   /// Allowed Values: `debuggerStatement`, `call`, `return`.
-  String get type => json['type'];
+  String? get type => json['type'] as String?;
 }
 
 /// The response from [WipDebugger.setBreakpoint].
 class SetBreakpointResponse extends WipResponse {
   SetBreakpointResponse(Map<String, dynamic> json) : super(json);
 
-  String get breakpointId => result['breakpointId'];
+  String get breakpointId => result!['breakpointId'];
 
-  WipLocation get actualLocation => WipLocation(result['actualLocation']);
+  WipLocation get actualLocation => WipLocation(result!['actualLocation']);
 }
diff --git a/lib/src/dom.dart b/lib/src/dom.dart
index d1fd1e5..7c5433f 100644
--- a/lib/src/dom.dart
+++ b/lib/src/dom.dart
@@ -14,52 +14,43 @@
   Future<Map<String, String>> getAttributes(int nodeId) async {
     WipResponse resp =
         await sendCommand('DOM.getAttributes', params: {'nodeId': nodeId});
-    return _attributeListToMap((resp.result['attributes'] as List).cast());
+    return _attributeListToMap((resp.result!['attributes'] as List).cast());
   }
 
   Future<Node> getDocument() async =>
-      new Node((await sendCommand('DOM.getDocument')).result['root']
+      Node((await sendCommand('DOM.getDocument')).result!['root']
           as Map<String, dynamic>);
 
   Future<String> getOuterHtml(int nodeId) async =>
       (await sendCommand('DOM.getOuterHTML', params: {'nodeId': nodeId}))
-          .result['root'] as String;
+          .result!['root'] as String;
 
-  Future hideHighlight() => sendCommand('DOM.hideHighlight');
+  Future<void> hideHighlight() => sendCommand('DOM.hideHighlight');
 
-  Future highlightNode(int nodeId,
-      {Rgba borderColor,
-      Rgba contentColor,
-      Rgba marginColor,
-      Rgba paddingColor,
-      bool showInfo}) {
-    var params = <String, dynamic>{'nodeId': nodeId, 'highlightConfig': {}};
-
-    if (borderColor != null) {
-      params['highlightConfig']['borderColor'] = borderColor;
-    }
-
-    if (contentColor != null) {
-      params['highlightConfig']['contentColor'] = contentColor;
-    }
-
-    if (marginColor != null) {
-      params['highlightConfig']['marginColor'] = marginColor;
-    }
-
-    if (paddingColor != null) {
-      params['highlightConfig']['paddingColor'] = paddingColor;
-    }
-
-    if (showInfo != null) {
-      params['highlightConfig']['showInfo'] = showInfo;
-    }
+  Future<void> highlightNode(
+    int nodeId, {
+    Rgba? borderColor,
+    Rgba? contentColor,
+    Rgba? marginColor,
+    Rgba? paddingColor,
+    bool? showInfo,
+  }) {
+    var params = <String, dynamic>{
+      'nodeId': nodeId,
+      'highlightConfig': <String, dynamic>{
+        if (borderColor != null) 'borderColor': borderColor,
+        if (contentColor != null) 'contentColor': contentColor,
+        if (marginColor != null) 'marginColor': marginColor,
+        if (paddingColor != null) 'paddingColor': paddingColor,
+        if (showInfo != null) 'showInfo': showInfo,
+      },
+    };
 
     return sendCommand('DOM.highlightNode', params: params);
   }
 
-  Future highlightRect(int x, int y, int width, int height,
-      {Rgba color, Rgba outlineColor}) {
+  Future<void> highlightRect(int x, int y, int width, int height,
+      {Rgba? color, Rgba? outlineColor}) {
     var params = <String, dynamic>{
       'x': x,
       'y': y,
@@ -79,7 +70,7 @@
   }
 
   Future<int> moveTo(int nodeId, int targetNodeId,
-      {int insertBeforeNodeId}) async {
+      {int? insertBeforeNodeId}) async {
     var params = {'nodeId': nodeId, 'targetNodeId': targetNodeId};
 
     if (insertBeforeNodeId != null) {
@@ -87,52 +78,52 @@
     }
 
     var resp = await sendCommand('DOM.moveTo', params: params);
-    return resp.result['nodeId'] as int;
+    return resp.result!['nodeId'] as int;
   }
 
   Future<int> querySelector(int nodeId, String selector) async {
     var resp = await sendCommand('DOM.querySelector',
         params: {'nodeId': nodeId, 'selector': selector});
-    return resp.result['nodeId'] as int;
+    return resp.result!['nodeId'] as int;
   }
 
   Future<List<int>> querySelectorAll(int nodeId, String selector) async {
     var resp = await sendCommand('DOM.querySelectorAll',
         params: {'nodeId': nodeId, 'selector': selector});
-    return (resp.result['nodeIds'] as List).cast();
+    return (resp.result!['nodeIds'] as List).cast();
   }
 
-  Future removeAttribute(int nodeId, String name) =>
+  Future<void> removeAttribute(int nodeId, String name) =>
       sendCommand('DOM.removeAttribute',
           params: {'nodeId': nodeId, 'name': name});
 
-  Future removeNode(int nodeId) =>
+  Future<void> removeNode(int nodeId) =>
       sendCommand('DOM.removeNode', params: {'nodeId': nodeId});
 
-  Future requestChildNodes(int nodeId) =>
+  Future<void> requestChildNodes(int nodeId) =>
       sendCommand('DOM.requestChildNodes', params: {'nodeId': nodeId});
 
   Future<int> requestNode(String objectId) async {
     var resp =
         await sendCommand('DOM.requestNode', params: {'objectId': objectId});
-    return resp.result['nodeId'] as int;
+    return resp.result!['nodeId'] as int;
   }
 
-  Future<RemoteObject> resolveNode(int nodeId, {String objectGroup}) async {
+  Future<RemoteObject> resolveNode(int nodeId, {String? objectGroup}) async {
     var params = <String, dynamic>{'nodeId': nodeId};
     if (objectGroup != null) {
       params['objectGroup'] = objectGroup;
     }
 
     var resp = await sendCommand('DOM.resolveNode', params: params);
-    return new RemoteObject(resp.result['object'] as Map<String, dynamic>);
+    return RemoteObject(resp.result!['object'] as Map<String, dynamic>);
   }
 
-  Future setAttributeValue(int nodeId, String name, String value) =>
+  Future<void> setAttributeValue(int nodeId, String name, String value) =>
       sendCommand('DOM.setAttributeValue',
           params: {'nodeId': nodeId, 'name': name, 'value': value});
 
-  Future setAttributesAsText(int nodeId, String text, {String name}) {
+  Future<void> setAttributesAsText(int nodeId, String text, {String? name}) {
     var params = {'nodeId': nodeId, 'text': text};
     if (name != null) {
       params['name'] = name;
@@ -143,106 +134,99 @@
   Future<int> setNodeName(int nodeId, String name) async {
     var resp = await sendCommand('DOM.setNodeName',
         params: {'nodeId': nodeId, 'name': name});
-    return resp.result['nodeId'] as int;
+    return resp.result!['nodeId'] as int;
   }
 
-  Future setNodeValue(int nodeId, String value) =>
+  Future<void> setNodeValue(int nodeId, String value) =>
       sendCommand('DOM.setNodeValue',
           params: {'nodeId': nodeId, 'value': value});
 
-  Future setOuterHtml(int nodeId, String outerHtml) =>
+  Future<void> setOuterHtml(int nodeId, String outerHtml) =>
       sendCommand('DOM.setOuterHTML',
           params: {'nodeId': nodeId, 'outerHtml': outerHtml});
 
   Stream<AttributeModifiedEvent> get onAttributeModified => eventStream(
       'DOM.attributeModified',
-      (WipEvent event) => new AttributeModifiedEvent(event.json));
+      (WipEvent event) => AttributeModifiedEvent(event.json));
 
   Stream<AttributeRemovedEvent> get onAttributeRemoved => eventStream(
       'DOM.attributeRemoved',
-      (WipEvent event) => new AttributeRemovedEvent(event.json));
+      (WipEvent event) => AttributeRemovedEvent(event.json));
 
   Stream<CharacterDataModifiedEvent> get onCharacterDataModified => eventStream(
       'DOM.characterDataModified',
-      (WipEvent event) => new CharacterDataModifiedEvent(event.json));
+      (WipEvent event) => CharacterDataModifiedEvent(event.json));
 
   Stream<ChildNodeCountUpdatedEvent> get onChildNodeCountUpdated => eventStream(
       'DOM.childNodeCountUpdated',
-      (WipEvent event) => new ChildNodeCountUpdatedEvent(event.json));
+      (WipEvent event) => ChildNodeCountUpdatedEvent(event.json));
 
   Stream<ChildNodeInsertedEvent> get onChildNodeInserted => eventStream(
       'DOM.childNodeInserted',
-      (WipEvent event) => new ChildNodeInsertedEvent(event.json));
+      (WipEvent event) => ChildNodeInsertedEvent(event.json));
 
   Stream<ChildNodeRemovedEvent> get onChildNodeRemoved => eventStream(
       'DOM.childNodeRemoved',
-      (WipEvent event) => new ChildNodeRemovedEvent(event.json));
+      (WipEvent event) => ChildNodeRemovedEvent(event.json));
 
   Stream<DocumentUpdatedEvent> get onDocumentUpdated => eventStream(
       'DOM.documentUpdated',
-      (WipEvent event) => new DocumentUpdatedEvent(event.json));
+      (WipEvent event) => DocumentUpdatedEvent(event.json));
 
   Stream<SetChildNodesEvent> get onSetChildNodes => eventStream(
-      'DOM.setChildNodes',
-      (WipEvent event) => new SetChildNodesEvent(event.json));
+      'DOM.setChildNodes', (WipEvent event) => SetChildNodesEvent(event.json));
 }
 
 class AttributeModifiedEvent extends WipEvent {
   AttributeModifiedEvent(Map<String, dynamic> json) : super(json);
 
-  int get nodeId => params['nodeId'] as int;
+  int get nodeId => params!['nodeId'] as int;
 
-  String get name => params['name'] as String;
+  String get name => params!['name'] as String;
 
-  String get value => params['value'] as String;
+  String get value => params!['value'] as String;
 }
 
 class AttributeRemovedEvent extends WipEvent {
   AttributeRemovedEvent(Map<String, dynamic> json) : super(json);
 
-  int get nodeId => params['nodeId'] as int;
+  int get nodeId => params!['nodeId'] as int;
 
-  String get name => params['name'] as String;
+  String get name => params!['name'] as String;
 }
 
 class CharacterDataModifiedEvent extends WipEvent {
   CharacterDataModifiedEvent(Map<String, dynamic> json) : super(json);
 
-  int get nodeId => params['nodeId'] as int;
+  int get nodeId => params!['nodeId'] as int;
 
-  String get characterData => params['characterData'] as String;
+  String get characterData => params!['characterData'] as String;
 }
 
 class ChildNodeCountUpdatedEvent extends WipEvent {
   ChildNodeCountUpdatedEvent(Map<String, dynamic> json) : super(json);
 
-  int get nodeId => params['nodeId'] as int;
+  int get nodeId => params!['nodeId'] as int;
 
-  int get childNodeCount => params['childNodeCount'] as int;
+  int get childNodeCount => params!['childNodeCount'] as int;
 }
 
 class ChildNodeInsertedEvent extends WipEvent {
   ChildNodeInsertedEvent(Map<String, dynamic> json) : super(json);
 
-  int get parentNodeId => params['parentNodeId'] as int;
+  int get parentNodeId => params!['parentNodeId'] as int;
 
-  int get previousNodeId => params['previousNodeId'] as int;
-  Node _node;
+  int get previousNodeId => params!['previousNodeId'] as int;
 
-  Node get node {
-    if (_node == null) {
-      _node = new Node(params['node'] as Map<String, dynamic>);
-    }
-    return _node;
-  }
+  late final node = Node(params!['node'] as Map<String, dynamic>);
 }
 
 class ChildNodeRemovedEvent extends WipEvent {
   ChildNodeRemovedEvent(Map<String, dynamic> json) : super(json);
 
-  int get parentNodeId => params['parentNodeId'] as int;
+  int get parentNodeId => params!['parentNodeId'] as int;
 
-  int get nodeId => params['nodeId'] as int;
+  int get nodeId => params!['nodeId'] as int;
 }
 
 class DocumentUpdatedEvent extends WipEvent {
@@ -252,14 +236,15 @@
 class SetChildNodesEvent extends WipEvent {
   SetChildNodesEvent(Map<String, dynamic> json) : super(json);
 
-  int get nodeId => params['parentId'] as int;
+  int get nodeId => params!['parentId'] as int;
 
   Iterable<Node> get nodes sync* {
-    for (Map node in params['nodes']) {
-      yield new Node(node as Map<String, dynamic>);
+    for (Map node in params!['nodes']) {
+      yield Node(node as Map<String, dynamic>);
     }
   }
 
+  @override
   String toString() => 'SetChildNodes $nodeId: $nodes';
 }
 
@@ -271,41 +256,31 @@
 
   Node(this._map);
 
-  Map<String, String> _attributes;
+  late final Map<String, String>? attributes = _map.containsKey('attributes')
+      ? _attributeListToMap((_map['attributes'] as List).cast())
+      : null;
 
-  Map<String, String> get attributes {
-    if (_attributes == null && _map.containsKey('attributes')) {
-      _attributes = _attributeListToMap((_map['attributes'] as List).cast());
-    }
-    return _attributes;
-  }
+  int? get childNodeCount => _map['childNodeCount'] as int?;
 
-  int get childNodeCount => _map['childNodeCount'] as int;
+  late final List<Node>? children = _map.containsKey('children')
+      ? UnmodifiableListView((_map['children'] as List)
+          .map((c) => Node(c as Map<String, dynamic>)))
+      : null;
 
-  List<Node> _children;
-
-  List<Node> get children {
-    if (_children == null && _map.containsKey('children')) {
-      _children = new UnmodifiableListView((_map['children'] as List)
-          .map((c) => new Node(c as Map<String, dynamic>)));
-    }
-    return _children;
-  }
-
-  Node get contentDocument {
+  Node? get contentDocument {
     if (_map.containsKey('contentDocument')) {
-      return new Node(_map['contentDocument'] as Map<String, dynamic>);
+      return Node(_map['contentDocument'] as Map<String, dynamic>);
     }
     return null;
   }
 
-  String get documentUrl => _map['documentURL'] as String;
+  String? get documentUrl => _map['documentURL'] as String?;
 
-  String get internalSubset => _map['internalSubset'] as String;
+  String? get internalSubset => _map['internalSubset'] as String?;
 
   String get localName => _map['localName'] as String;
 
-  String get name => _map['name'] as String;
+  String? get name => _map['name'] as String?;
 
   int get nodeId => _map['nodeId'] as int;
 
@@ -315,29 +290,30 @@
 
   String get nodeValue => _map['nodeValue'] as String;
 
-  String get publicId => _map['publicId'] as String;
+  String? get publicId => _map['publicId'] as String?;
 
-  String get systemId => _map['systemId'] as String;
+  String? get systemId => _map['systemId'] as String?;
 
-  String get value => _map['value'] as String;
+  String? get value => _map['value'] as String?;
 
-  String get xmlVersion => _map['xmlVersion'] as String;
+  String? get xmlVersion => _map['xmlVersion'] as String?;
 
+  @override
   String toString() => '$nodeName: $nodeId $attributes';
 }
 
 class Rgba {
-  final int a;
+  final int? a;
   final int b;
   final int r;
   final int g;
 
   Rgba(this.r, this.g, this.b, [this.a]);
 
-  Map toJson() {
+  Map<String, int> toJson() {
     var json = {'r': r, 'g': g, 'b': b};
     if (a != null) {
-      json['a'] = a;
+      json['a'] = a!;
     }
     return json;
   }
@@ -348,5 +324,5 @@
   for (int i = 0; i < attrList.length; i += 2) {
     attributes[attrList[i]] = attrList[i + 1];
   }
-  return new UnmodifiableMapView(attributes);
+  return UnmodifiableMapView(attributes);
 }
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 6dc5b46..9839472 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -12,14 +12,14 @@
 
   Future<WipResponse> disable() => sendCommand('Log.disable');
 
-  Stream<LogEntry> get onEntryAdded => eventStream(
-      'Log.entryAdded', (WipEvent event) => new LogEntry(event.json));
+  Stream<LogEntry> get onEntryAdded =>
+      eventStream('Log.entryAdded', (WipEvent event) => LogEntry(event.json));
 }
 
 class LogEntry extends WipEvent {
   LogEntry(Map<String, dynamic> json) : super(json);
 
-  Map<String, dynamic> get _entry => params['entry'] as Map<String, dynamic>;
+  Map<String, dynamic> get _entry => params!['entry'] as Map<String, dynamic>;
 
   /// Log entry source. Allowed values: xml, javascript, network, storage,
   /// appcache, rendering, security, deprecation, worker, violation,
@@ -34,10 +34,11 @@
 
   /// URL of the resource if known.
   @optional
-  String get url => _entry['url'] as String;
+  String? get url => _entry['url'] as String?;
 
   /// Timestamp when this entry was added.
   num get timestamp => _entry['timestamp'] as num;
 
+  @override
   String toString() => text;
 }
diff --git a/lib/src/page.dart b/lib/src/page.dart
index e8bf1e5..5b125d0 100644
--- a/lib/src/page.dart
+++ b/lib/src/page.dart
@@ -16,7 +16,7 @@
       sendCommand('Page.navigate', params: {'url': url});
 
   Future<WipResponse> reload(
-      {bool ignoreCache, String scriptToEvaluateOnLoad}) {
+      {bool? ignoreCache, String? scriptToEvaluateOnLoad}) {
     var params = <String, dynamic>{};
     if (ignoreCache != null) {
       params['ignoreCache'] = ignoreCache;
diff --git a/lib/src/runtime.dart b/lib/src/runtime.dart
index da09256..ada5399 100644
--- a/lib/src/runtime.dart
+++ b/lib/src/runtime.dart
@@ -28,9 +28,9 @@
   ///     return once awaited promise is resolved.
   Future<RemoteObject> evaluate(
     String expression, {
-    bool returnByValue,
-    int contextId,
-    bool awaitPromise,
+    bool? returnByValue,
+    int? contextId,
+    bool? awaitPromise,
   }) async {
     Map<String, dynamic> params = {
       'expression': expression,
@@ -48,12 +48,11 @@
     final WipResponse response =
         await sendCommand('Runtime.evaluate', params: params);
 
-    if (response.result.containsKey('exceptionDetails')) {
-      throw new ExceptionDetails(
-          response.result['exceptionDetails'] as Map<String, dynamic>);
+    if (response.result!.containsKey('exceptionDetails')) {
+      throw ExceptionDetails(
+          response.result!['exceptionDetails'] as Map<String, dynamic>);
     } else {
-      return new RemoteObject(
-          response.result['result'] as Map<String, dynamic>);
+      return RemoteObject(response.result!['result'] as Map<String, dynamic>);
     }
   }
 
@@ -64,10 +63,10 @@
   /// object (int, String, double, bool).
   Future<RemoteObject> callFunctionOn(
     String functionDeclaration, {
-    String objectId,
-    List<dynamic> arguments,
-    bool returnByValue,
-    int executionContextId,
+    String? objectId,
+    List<dynamic>? arguments,
+    bool? returnByValue,
+    int? executionContextId,
   }) async {
     Map<String, dynamic> params = {
       'functionDeclaration': functionDeclaration,
@@ -95,12 +94,11 @@
     final WipResponse response =
         await sendCommand('Runtime.callFunctionOn', params: params);
 
-    if (response.result.containsKey('exceptionDetails')) {
-      throw new ExceptionDetails(
-          response.result['exceptionDetails'] as Map<String, dynamic>);
+    if (response.result!.containsKey('exceptionDetails')) {
+      throw ExceptionDetails(
+          response.result!['exceptionDetails'] as Map<String, dynamic>);
     } else {
-      return new RemoteObject(
-          response.result['result'] as Map<String, dynamic>);
+      return RemoteObject(response.result!['result'] as Map<String, dynamic>);
     }
   }
 
@@ -109,13 +107,13 @@
   @experimental
   Future<HeapUsage> getHeapUsage() async {
     final WipResponse response = await sendCommand('Runtime.getHeapUsage');
-    return HeapUsage(response.result);
+    return HeapUsage(response.result!);
   }
 
   /// Returns the isolate id.
   @experimental
   Future<String> getIsolateId() async {
-    return (await sendCommand('Runtime.getIsolateId')).result['id'] as String;
+    return (await sendCommand('Runtime.getIsolateId')).result!['id'] as String;
   }
 
   /// Returns properties of a given object. Object group of the result is
@@ -127,7 +125,7 @@
   /// itself, not to its prototype chain.
   Future<List<PropertyDescriptor>> getProperties(
     RemoteObject object, {
-    bool ownProperties,
+    bool? ownProperties,
   }) async {
     Map<String, dynamic> params = {
       'objectId': object.objectId,
@@ -139,34 +137,34 @@
     final WipResponse response =
         await sendCommand('Runtime.getProperties', params: params);
 
-    if (response.result.containsKey('exceptionDetails')) {
-      throw new ExceptionDetails(
-          response.result['exceptionDetails'] as Map<String, dynamic>);
+    if (response.result!.containsKey('exceptionDetails')) {
+      throw ExceptionDetails(
+          response.result!['exceptionDetails'] as Map<String, dynamic>);
     } else {
-      List locations = response.result['result'];
+      List locations = response.result!['result'];
       return List.from(locations.map((map) => PropertyDescriptor(map)));
     }
   }
 
   Stream<ConsoleAPIEvent> get onConsoleAPICalled => eventStream(
       'Runtime.consoleAPICalled',
-      (WipEvent event) => new ConsoleAPIEvent(event.json));
+      (WipEvent event) => ConsoleAPIEvent(event.json));
 
   Stream<ExceptionThrownEvent> get onExceptionThrown => eventStream(
       'Runtime.exceptionThrown',
-      (WipEvent event) => new ExceptionThrownEvent(event.json));
+      (WipEvent event) => ExceptionThrownEvent(event.json));
 
   /// Issued when new execution context is created.
   Stream<ExecutionContextDescription> get onExecutionContextCreated =>
       eventStream(
           'Runtime.executionContextCreated',
           (WipEvent event) =>
-              new ExecutionContextDescription(event.params['context']));
+              ExecutionContextDescription(event.params!['context']));
 
   /// Issued when execution context is destroyed.
   Stream<String> get onExecutionContextDestroyed => eventStream(
       'Runtime.executionContextDestroyed',
-      (WipEvent event) => event.params['executionContextId']);
+      (WipEvent event) => event.params!['executionContextId']);
 
   /// Issued when all executionContexts were cleared in browser.
   Stream get onExecutionContextsCleared => eventStream(
@@ -180,14 +178,14 @@
   /// Type of the call. Allowed values: log, debug, info, error, warning, dir,
   /// dirxml, table, trace, clear, startGroup, startGroupCollapsed, endGroup,
   /// assert, profile, profileEnd.
-  String get type => params['type'] as String;
+  String get type => params!['type'] as String;
 
   /// Call timestamp.
-  num get timestamp => params['timestamp'] as num;
+  num get timestamp => params!['timestamp'] as num;
 
   /// Call arguments.
-  List<RemoteObject> get args => (params['args'] as List)
-      .map((m) => new RemoteObject(m as Map<String, dynamic>))
+  List<RemoteObject> get args => (params!['args'] as List)
+      .map((m) => RemoteObject(m as Map<String, dynamic>))
       .toList();
 }
 
@@ -212,10 +210,10 @@
   ExceptionThrownEvent(Map<String, dynamic> json) : super(json);
 
   /// Timestamp of the exception.
-  int get timestamp => params['timestamp'] as int;
+  int get timestamp => params!['timestamp'] as int;
 
   ExceptionDetails get exceptionDetails =>
-      new ExceptionDetails(params['exceptionDetails'] as Map<String, dynamic>);
+      ExceptionDetails(params!['exceptionDetails'] as Map<String, dynamic>);
 }
 
 class ExceptionDetails implements Exception {
@@ -243,20 +241,21 @@
 
   /// Script ID of the exception location.
   @optional
-  String get scriptId => json['scriptId'] as String;
+  String? get scriptId => json['scriptId'] as String?;
 
   /// JavaScript stack trace if available.
   @optional
-  StackTrace get stackTrace => json['stackTrace'] == null
+  StackTrace? get stackTrace => json['stackTrace'] == null
       ? null
-      : new StackTrace(json['stackTrace'] as Map<String, dynamic>);
+      : StackTrace(json['stackTrace'] as Map<String, dynamic>);
 
   /// Exception object if available.
   @optional
-  RemoteObject get exception => json['exception'] == null
+  RemoteObject? get exception => json['exception'] == null
       ? null
-      : new RemoteObject(json['exception'] as Map<String, dynamic>);
+      : RemoteObject(json['exception'] as Map<String, dynamic>);
 
+  @override
   String toString() => '$text, $url, $scriptId, $lineNumber, $exception';
 }
 
@@ -267,7 +266,7 @@
   StackTrace(this.json);
 
   List<CallFrame> get callFrames => (json['callFrames'] as List)
-      .map((m) => new CallFrame(m as Map<String, dynamic>))
+      .map((m) => CallFrame(m as Map<String, dynamic>))
       .toList();
 
   /// String label of this stack trace. For async traces this may be a name of
@@ -278,7 +277,7 @@
   /// Asynchronous JavaScript stack trace that preceded this stack, if
   /// available.
   @optional
-  StackTrace get parent {
+  StackTrace? get parent {
     return json['parent'] == null ? null : StackTrace(json['parent']);
   }
 
@@ -290,11 +289,12 @@
     });
 
     return frames.map((CallFrame frame) {
-      return '${frame.functionName}()'.padRight(width + 2) +
-          ' ${frame.url} ${frame.lineNumber}:${frame.columnNumber}';
+      var name = '${frame.functionName}()'.padRight(width + 2);
+      return '$name ${frame.url} ${frame.lineNumber}:${frame.columnNumber}';
     }).toList();
   }
 
+  @override
   String toString() => callFrames.map((f) => '  $f').join('\n');
 }
 
@@ -321,6 +321,7 @@
   /// JavaScript script column number (0-based).
   int get columnNumber => json['columnNumber'] as int;
 
+  @override
   String toString() => '$functionName() ($url $lineNumber:$columnNumber)';
 }
 
@@ -341,22 +342,22 @@
   /// Allowed Values: array, null, node, regexp, date, map, set, weakmap,
   /// weakset, iterator, generator, error, proxy, promise, typedarray,
   /// arraybuffer, dataview, i32, i64, f32, f64, v128, anyref.
-  String get subtype => json['subtype'] as String;
+  String? get subtype => json['subtype'] as String?;
 
   /// Object class (constructor) name.
   ///
   /// Specified for object type values only.
-  String get className => json['className'] as String;
+  String? get className => json['className'] as String?;
 
   /// Remote object value in case of primitive values or JSON values (if it was
   /// requested). (optional)
-  Object get value => json['value'];
+  Object? get value => json['value'];
 
   /// String representation of the object. (optional)
-  String get description => json['description'] as String;
+  String? get description => json['description'] as String?;
 
   /// Unique object identifier (for non-primitive values). (optional)
-  String get objectId => json['objectId'] as String;
+  String? get objectId => json['objectId'] as String?;
 
   @override
   String toString() => '$type $value';
@@ -389,24 +390,24 @@
   String get name => json['name'];
 
   /// The value associated with the property.
-  RemoteObject get value =>
+  RemoteObject? get value =>
       json['value'] != null ? RemoteObject(json['value']) : null;
 
   /// True if the value associated with the property may be changed (data
   /// descriptors only).
-  bool get writable => json['writable'];
+  bool? get writable => json['writable'] as bool?;
 
   /// True if the type of this property descriptor may be changed and if the
   /// property may be deleted from the corresponding object.
-  bool get configurable => json['configurable'];
+  bool get configurable => json['configurable'] as bool;
 
   /// True if this property shows up during enumeration of the properties on the
   /// corresponding object.
-  bool get enumerable => json['enumerable'];
+  bool get enumerable => json['enumerable'] as bool;
 
   /// True if the result was thrown during the evaluation.
-  bool get wasThrown => json['wasThrown'];
+  bool? get wasThrown => json['wasThrown'] as bool?;
 
   /// True if the property is owned for the object.
-  bool get isOwn => json['isOwn'];
+  bool? get isOwn => json['isOwn'] as bool?;
 }
diff --git a/lib/src/target.dart b/lib/src/target.dart
index 5b51382..41a04c1 100644
--- a/lib/src/target.dart
+++ b/lib/src/target.dart
@@ -16,7 +16,7 @@
   Future<String> createTarget(String url) async {
     WipResponse response =
         await sendCommand('Target.createTarget', params: {'url': url});
-    return response.result['targetId'] as String;
+    return response.result!['targetId'] as String;
   }
 
   /// Activates (focuses) the target.
@@ -29,7 +29,7 @@
   Future<bool> closeTarget(String targetId) async {
     WipResponse response =
         await sendCommand('Target.closeTarget', params: {'targetId': targetId});
-    return response.result['success'] as bool;
+    return response.result!['success'] as bool;
   }
 
   /// Inject object to the target's main frame that provides a communication
@@ -43,7 +43,7 @@
   @experimental
   Future<WipResponse> exposeDevToolsProtocol(
     String targetId, {
-    String bindingName,
+    String? bindingName,
   }) {
     final Map<String, dynamic> params = {'targetId': targetId};
     if (bindingName != null) {
diff --git a/lib/webkit_inspection_protocol.dart b/lib/webkit_inspection_protocol.dart
index 1a0330e..6f5f7df 100644
--- a/lib/webkit_inspection_protocol.dart
+++ b/lib/webkit_inspection_protocol.dart
@@ -6,7 +6,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io' show HttpClient, HttpClientResponse, WebSocket;
+import 'dart:io' show HttpClient, HttpClientResponse, IOException, WebSocket;
 
 import 'src/console.dart';
 import 'src/debugger.dart';
@@ -29,24 +29,57 @@
 /// This assumes the browser has been started with the `--remote-debugging-port`
 /// flag. The data is read from the `http://{host}:{port}/json` url.
 class ChromeConnection {
-  final HttpClient _client = new HttpClient();
+  final HttpClient _client = HttpClient();
 
   final Uri url;
 
   ChromeConnection(String host, [int port = 9222])
-      : url = Uri.parse('http://${host}:${port}/');
+      : url = Uri.parse('http://$host:$port/');
 
-  // TODO(DrMarcII): consider changing this to return Stream<ChromeTab>.
-  Future<List<ChromeTab>> getTabs() async {
+  /// Return all the available tabs.
+  ///
+  /// This method can potentially throw a [ConnectionException] on some protocol
+  /// issues.
+  ///
+  /// An optional [retryFor] duration can be used to automatically re-try
+  /// connections for some period of time. Anecdotally, Chrome can return errors
+  /// when trying to list the available tabs very early in its startup sequence.
+  Future<List<ChromeTab>> getTabs({
+    Duration? retryFor,
+  }) async {
+    final start = DateTime.now();
+    DateTime? end = retryFor == null ? null : start.add(retryFor);
+
     var response = await getUrl('/json');
-    var respBody = await utf8.decodeStream(response.cast<List<int>>());
-    return new List<ChromeTab>.from(
-        (jsonDecode(respBody) as List).map((m) => new ChromeTab(m as Map)));
+    var responseBody = await utf8.decodeStream(response.cast<List<int>>());
+
+    late List decoded;
+    while (true) {
+      try {
+        decoded = jsonDecode(responseBody);
+        return List<ChromeTab>.from(decoded.map((m) => ChromeTab(m as Map)));
+      } on FormatException catch (formatException) {
+        if (end != null && end.isBefore(DateTime.now())) {
+          // Delay for retryFor / 4 milliseconds.
+          await Future.delayed(
+            Duration(milliseconds: retryFor!.inMilliseconds ~/ 4),
+          );
+        } else {
+          throw ConnectionException(
+            formatException: formatException,
+            responseStatus: '${response.statusCode} ${response.reasonPhrase}',
+            responseBody: responseBody,
+          );
+        }
+      }
+    }
   }
 
-  Future<ChromeTab> getTab(bool accept(ChromeTab tab),
-      {Duration retryFor}) async {
-    var start = new DateTime.now();
+  Future<ChromeTab?> getTab(
+    bool Function(ChromeTab tab) accept, {
+    Duration? retryFor,
+  }) async {
+    var start = DateTime.now();
     var end = start;
     if (retryFor != null) {
       end = start.add(retryFor);
@@ -59,15 +92,15 @@
             return tab;
           }
         }
-        if (end.isBefore(new DateTime.now())) {
+        if (end.isBefore(DateTime.now())) {
           return null;
         }
       } catch (e) {
-        if (end.isBefore(new DateTime.now())) {
+        if (end.isBefore(DateTime.now())) {
           rethrow;
         }
       }
-      await new Future.delayed(const Duration(milliseconds: 25));
+      await Future.delayed(const Duration(milliseconds: 25));
     }
   }
 
@@ -79,21 +112,54 @@
   void close() => _client.close(force: true);
 }
 
+/// An exception that can be thrown early in the connection sequence for a
+/// [ChromeConnection].
+///
+/// This exception includes the underlying exception, as well as the http
+/// response from the browser that we failed on. The [toString] implementation
+/// includes a summary of the response.
+class ConnectionException implements IOException {
+  final FormatException formatException;
+  final String responseStatus;
+  final String responseBody;
+
+  ConnectionException({
+    required this.formatException,
+    required this.responseStatus,
+    required this.responseBody,
+  });
+
+  @override
+  String toString() {
+    final buf = StringBuffer('${formatException.message}\n');
+    buf.writeln('$responseStatus; body:');
+    var lines = responseBody.split('\n');
+    if (lines.length > 10) {
+      lines = [
+        ...lines.take(10),
+        '...',
+      ];
+    }
+    buf.writeAll(lines, '\n');
+    return buf.toString();
+  }
+}
+
 class ChromeTab {
   final Map _map;
 
   ChromeTab(this._map);
 
-  String get description => _map['description'] as String;
+  String? get description => _map['description'] as String?;
 
-  String get devtoolsFrontendUrl => _map['devtoolsFrontendUrl'] as String;
+  String? get devtoolsFrontendUrl => _map['devtoolsFrontendUrl'] as String?;
 
-  String get faviconUrl => _map['faviconUrl'] as String;
+  String? get faviconUrl => _map['faviconUrl'] as String?;
 
   /// Ex. `E1999E8A-EE27-0450-9900-5BFF4C69CA83`.
   String get id => _map['id'] as String;
 
-  String get title => _map['title'] as String;
+  String? get title => _map['title'] as String?;
 
   /// Ex. `background_page`, `page`.
   String get type => _map['type'] as String;
@@ -112,6 +178,7 @@
   Future<WipConnection> connect() =>
       WipConnection.connect(webSocketDebuggerUrl);
 
+  @override
   String toString() => url;
 }
 
@@ -124,59 +191,38 @@
 
   int _nextId = 0;
 
-  WipConsole _console; // ignore: deprecated_member_use
   @Deprecated('This domain is deprecated - use Runtime or Log instead')
-  WipConsole get console => _console;
+  late final WipConsole console = WipConsole(this);
 
-  WipDebugger _debugger;
+  late final WipDebugger debugger = WipDebugger(this);
 
-  WipDebugger get debugger => _debugger;
+  late final WipDom dom = WipDom(this);
 
-  WipDom _dom;
+  late final WipPage page = WipPage(this);
 
-  WipDom get dom => _dom;
+  late final WipTarget target = WipTarget(this);
 
-  WipPage _page;
+  late final WipLog log = WipLog(this);
 
-  WipPage get page => _page;
-
-  WipTarget _target;
-
-  WipTarget get target => _target;
-
-  WipLog _log;
-
-  WipLog get log => _log;
-
-  WipRuntime _runtime;
-
-  WipRuntime get runtime => _runtime;
+  late final WipRuntime runtime = WipRuntime(this);
 
   final StreamController<String> _onSend =
       StreamController.broadcast(sync: true);
   final StreamController<String> _onReceive =
       StreamController.broadcast(sync: true);
 
-  final Map _completers = <int, Completer<WipResponse>>{};
+  final Map<int, Completer<WipResponse>> _completers = {};
 
-  final _closeController = new StreamController<WipConnection>.broadcast();
-  final _notificationController = new StreamController<WipEvent>.broadcast();
+  final _closeController = StreamController<WipConnection>.broadcast();
+  final _notificationController = StreamController<WipEvent>.broadcast();
 
   static Future<WipConnection> connect(String url) {
     return WebSocket.connect(url).then((socket) {
-      return new WipConnection._(url, socket);
+      return WipConnection._(url, socket);
     });
   }
 
   WipConnection._(this.url, this._ws) {
-    _console = new WipConsole(this); // ignore: deprecated_member_use
-    _debugger = new WipDebugger(this);
-    _dom = new WipDom(this);
-    _page = new WipPage(this);
-    _target = new WipTarget(this);
-    _log = new WipLog(this);
-    _runtime = new WipRuntime(this);
-
     _ws.listen((data) {
       var json = jsonDecode(data as String) as Map<String, dynamic>;
       _onReceive.add(data);
@@ -195,16 +241,17 @@
 
   Future close() => _ws.close();
 
+  @override
   String toString() => url;
 
   Future<WipResponse> sendCommand(String method,
-      [Map<String, dynamic> params]) {
-    var completer = new Completer<WipResponse>();
+      [Map<String, dynamic>? params]) {
+    var completer = Completer<WipResponse>();
     var json = {'id': _nextId++, 'method': method};
     if (params != null) {
       json['params'] = params;
     }
-    _completers[json['id']] = completer;
+    _completers[json['id'] as int] = completer;
     String message = jsonEncode(json);
     _ws.add(message);
     _onSend.add(message);
@@ -212,16 +259,16 @@
   }
 
   void _handleNotification(Map<String, dynamic> json) {
-    _notificationController.add(new WipEvent(json));
+    _notificationController.add(WipEvent(json));
   }
 
   void _handleResponse(Map<String, dynamic> event) {
-    var completer = _completers.remove(event['id']);
+    var completer = _completers.remove(event['id'])!;
 
     if (event.containsKey('error')) {
-      completer.completeError(new WipError(event));
+      completer.completeError(WipError(event));
     } else {
-      completer.complete(new WipResponse(event));
+      completer.complete(WipResponse(event));
     }
   }
 
@@ -242,12 +289,13 @@
   final Map<String, dynamic> json;
 
   final String method;
-  final Map<String, dynamic> params;
+  final Map<String, dynamic>? params;
 
   WipEvent(this.json)
       : method = json['method'] as String,
-        params = json['params'] as Map<String, dynamic>;
+        params = json['params'] as Map<String, dynamic>?;
 
+  @override
   String toString() => 'WipEvent: $method($params)';
 }
 
@@ -255,16 +303,17 @@
   final Map<String, dynamic> json;
 
   final int id;
-  final dynamic error;
+  final Map<String, dynamic>? error;
 
   WipError(this.json)
       : id = json['id'] as int,
-        error = json['error'];
+        error = json['error'] as Map<String, dynamic>?;
 
-  int get code => error == null ? null : error['code'];
+  int? get code => error == null ? null : error!['code'];
 
-  String get message => error == null ? null : error['message'];
+  String? get message => error == null ? null : error!['message'];
 
+  @override
   String toString() => 'WipError $code $message';
 }
 
@@ -272,16 +321,17 @@
   final Map<String, dynamic> json;
 
   final int id;
-  final Map<String, dynamic> result;
+  final Map<String, dynamic>? result;
 
   WipResponse(this.json)
       : id = json['id'] as int,
-        result = json['result'] as Map<String, dynamic>;
+        result = json['result'] as Map<String, dynamic>?;
 
+  @override
   String toString() => 'WipResponse $id: $result';
 }
 
-typedef T WipEventTransformer<T>(WipEvent event);
+typedef WipEventTransformer<T> = T Function(WipEvent event);
 
 /// @optional
 const String optional = 'optional';
@@ -290,22 +340,19 @@
   final Map<String, Stream> _eventStreams = {};
 
   final WipConnection connection;
-  Stream<WipDomain> _onClosed;
 
-  Stream<WipDomain> get onClosed => _onClosed;
+  late final Stream<WipDomain> onClosed = StreamTransformer.fromHandlers(
+      handleData: (event, EventSink<WipDomain> sink) {
+    sink.add(this);
+  }).bind(connection.onClose);
 
-  WipDomain(WipConnection connection) : this.connection = connection {
-    this._onClosed = new StreamTransformer.fromHandlers(
-        handleData: (event, EventSink<WipDomain> sink) {
-      sink.add(this);
-    }).bind(connection.onClose);
-  }
+  WipDomain(this.connection);
 
   Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) {
     return _eventStreams
         .putIfAbsent(
           method,
-          () => new StreamTransformer.fromHandlers(
+          () => StreamTransformer.fromHandlers(
             handleData: (WipEvent event, EventSink<T> sink) {
               if (event.method == method) {
                 sink.add(transformer(event));
@@ -318,13 +365,14 @@
 
   Future<WipResponse> sendCommand(
     String method, {
-    Map<String, dynamic> params,
+    Map<String, dynamic>? params,
   }) {
     return connection.sendCommand(method, params);
   }
 }
 
-const _Experimental experimental = const _Experimental();
+// ignore: library_private_types_in_public_api
+const _Experimental experimental = _Experimental();
 
 class _Experimental {
   const _Experimental();
diff --git a/pubspec.yaml b/pubspec.yaml
index f08319e..a36f2e6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,18 +1,22 @@
 name: webkit_inspection_protocol
-version: 0.7.5
-description: A client for the Chrome DevTools Protocol (previously called the Webkit Inspection Protocol).
-homepage: https://github.com/google/webkit_inspection_protocol.dart
+version: 1.1.0-dev
+description: >
+  A client for the Chrome DevTools Protocol (previously called the Webkit
+  Inspection Protocol).
+repository: https://github.com/google/webkit_inspection_protocol.dart
 
 environment:
-  sdk: '>=2.0.0 <3.0.0'
+  sdk: '>=2.12.0 <3.0.0'
 
 dependencies:
-  logging: '>=0.11.0 <2.0.0'
+  logging: ^1.0.0
 
 dev_dependencies:
-  args: '>=0.13.0 <2.0.0'
-  shelf: ^0.7.0
-  shelf_static: ^0.2.0
-  shelf_web_socket: ^0.2.0
+  args: ^2.0.0
+  lints: ^2.0.0
+  shelf_static: ^1.0.0
+  shelf_web_socket: ^1.0.0
+  shelf: ^1.0.0
   test: ^1.16.0
-  webdriver: ^2.0.0
+  web_socket_channel: ^2.0.0 # used in example/
+  webdriver: ^3.0.0
diff --git a/test/console_test.dart b/test/console_test.dart
index 46c5abf..e4f68a0 100644
--- a/test/console_test.dart
+++ b/test/console_test.dart
@@ -13,13 +13,13 @@
 
 void main() {
   group('WipConsole', () {
-    WipConsole console; // ignore: deprecated_member_use
+    WipConsole? console; // ignore: deprecated_member_use
     List<ConsoleMessageEvent> events = [];
-    var subs = [];
+    var subs = <StreamSubscription>[];
 
     Future checkMessages(int expectedCount) async {
       // make sure all messages have been delivered
-      await new Future.delayed(const Duration(seconds: 1));
+      await Future.delayed(const Duration(seconds: 1));
       expect(events, hasLength(expectedCount));
       for (int i = 0; i < expectedCount; i++) {
         if (i == 0) {
@@ -35,26 +35,28 @@
       // ignore: deprecated_member_use
       console = (await wipConnection).console;
       events.clear();
-      subs.add(console.onMessage.listen(events.add));
+      subs.add(console!.onMessage.listen(events.add));
     });
 
     tearDown(() async {
-      await console.disable();
+      await console?.disable();
       console = null;
       await closeConnection();
-      subs.forEach((s) => s.cancel());
+      for (var s in subs) {
+        s.cancel();
+      }
       subs.clear();
     });
 
     test('receives new console messages', () async {
-      await console.enable();
+      await console!.enable();
       await navigateToPage('console_test.html');
       await checkMessages(4);
     });
 
     test('receives old console messages', () async {
       await navigateToPage('console_test.html');
-      await console.enable();
+      await console!.enable();
       await checkMessages(4);
     });
 
diff --git a/test/debugger_test.dart b/test/debugger_test.dart
index 8c2c265..b9e691c 100644
--- a/test/debugger_test.dart
+++ b/test/debugger_test.dart
@@ -13,7 +13,7 @@
 
 void main() {
   group('WipDebugger', () {
-    WipDebugger debugger;
+    WipDebugger? debugger;
     List<StreamSubscription> subs = [];
 
     setUp(() async {
@@ -21,19 +21,21 @@
     });
 
     tearDown(() async {
-      await debugger.disable();
+      await debugger?.disable();
       debugger = null;
 
       await closeConnection();
-      subs.forEach((s) => s.cancel());
+      for (var s in subs) {
+        s.cancel();
+      }
       subs.clear();
     });
 
     test('gets script events', () async {
       final controller = StreamController<ScriptParsedEvent>();
-      subs.add(debugger.onScriptParsed.listen(controller.add));
+      subs.add(debugger!.onScriptParsed.listen(controller.add));
 
-      await debugger.enable();
+      await debugger!.enable();
       await navigateToPage('debugger_test.html');
 
       expect(controller.stream.first, isNotNull);
@@ -41,24 +43,24 @@
 
     test('getScriptSource', () async {
       final controller = StreamController<ScriptParsedEvent>();
-      subs.add(debugger.onScriptParsed.listen(controller.add));
+      subs.add(debugger!.onScriptParsed.listen(controller.add));
 
-      await debugger.enable();
+      await debugger!.enable();
       await navigateToPage('debugger_test.html');
 
       final event = await controller.stream
           .firstWhere((event) => event.script.url.endsWith('.html'));
       expect(event.script.scriptId, isNotEmpty);
 
-      final source = await debugger.getScriptSource(event.script.scriptId);
+      final source = await debugger!.getScriptSource(event.script.scriptId);
       expect(source, isNotEmpty);
     });
 
     test('getPossibleBreakpoints', () async {
       final controller = StreamController<ScriptParsedEvent>();
-      subs.add(debugger.onScriptParsed.listen(controller.add));
+      subs.add(debugger!.onScriptParsed.listen(controller.add));
 
-      await debugger.enable();
+      await debugger!.enable();
       await navigateToPage('debugger_test.html');
 
       final event = await controller.stream
@@ -67,7 +69,7 @@
 
       final script = event.script;
 
-      final result = await debugger
+      final result = await debugger!
           .getPossibleBreakpoints(WipLocation.fromValues(script.scriptId, 0));
       expect(result, isNotEmpty);
       expect(result.any((bp) => bp.lineNumber == 10), true);
@@ -75,9 +77,9 @@
 
     test('setBreakpoint / removeBreakpoint', () async {
       final controller = StreamController<ScriptParsedEvent>();
-      subs.add(debugger.onScriptParsed.listen(controller.add));
+      subs.add(debugger!.onScriptParsed.listen(controller.add));
 
-      await debugger.enable();
+      await debugger!.enable();
       await navigateToPage('debugger_test.html');
 
       final event = await controller.stream
@@ -86,11 +88,11 @@
 
       final script = event.script;
 
-      final bpResult = await debugger
+      final bpResult = await debugger!
           .setBreakpoint(WipLocation.fromValues(script.scriptId, 10));
       expect(bpResult.breakpointId, isNotEmpty);
 
-      final result = await debugger.removeBreakpoint(bpResult.breakpointId);
+      final result = await debugger!.removeBreakpoint(bpResult.breakpointId);
       expect(result.result, isEmpty);
     });
   });
diff --git a/test/dom_model_test.dart b/test/dom_model_test.dart
index 762d128..5ac2b53 100644
--- a/test/dom_model_test.dart
+++ b/test/dom_model_test.dart
@@ -12,10 +12,10 @@
 
 void main() {
   group('WipDomModel', () {
-    WipDom dom;
+    WipDom? dom;
 
     setUp(() async {
-      dom = new WipDomModel((await navigateToPage('dom_model_test.html')).dom);
+      dom = WipDomModel((await navigateToPage('dom_model_test.html')).dom);
     });
 
     tearDown(() async {
@@ -24,46 +24,46 @@
     });
 
     test('maintains model across getDocument calls', () async {
-      var document1 = await dom.getDocument();
-      var document2 = await dom.getDocument();
+      var document1 = await dom!.getDocument();
+      var document2 = await dom!.getDocument();
       expect(document2.nodeId, document1.nodeId);
     });
 
     test('requestChildNodes updates children', () async {
-      Node htmlNode = (await dom.getDocument()).children[1];
-      for (var child in htmlNode.children) {
+      Node htmlNode = (await dom!.getDocument()).children![1];
+      for (var child in htmlNode.children!) {
         expect(child.children, isNull);
-        await dom.requestChildNodes(child.nodeId);
+        await dom!.requestChildNodes(child.nodeId);
       }
       // wait for children to be updated
-      for (var child in htmlNode.children) {
+      for (var child in htmlNode.children!) {
         expect(child.children, isNotNull);
       }
     });
 
     test('removing a node updates children', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
-      await dom.requestChildNodes(bodyNode.nodeId);
-      var childCount = bodyNode.childNodeCount;
-      await dom.removeNode(bodyNode.children.first.nodeId);
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
+      await dom!.requestChildNodes(bodyNode.nodeId);
+      var childCount = bodyNode.childNodeCount!;
+      await dom!.removeNode(bodyNode.children!.first.nodeId);
 
       expect(bodyNode.children, hasLength(childCount - 1));
       expect(bodyNode.childNodeCount, childCount - 1);
     });
 
     test('Moving a node updates children', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
-      await dom.requestChildNodes(bodyNode.nodeId);
-      Node div1 = bodyNode.children[0];
-      Node div2 = bodyNode.children[1];
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
+      await dom!.requestChildNodes(bodyNode.nodeId);
+      Node div1 = bodyNode.children![0];
+      Node div2 = bodyNode.children![1];
 
       expect(div1.childNodeCount, 1);
       expect(div2.childNodeCount, 0);
 
-      await dom.requestChildNodes(div1.nodeId);
-      await dom.requestChildNodes(div2.nodeId);
+      await dom!.requestChildNodes(div1.nodeId);
+      await dom!.requestChildNodes(div2.nodeId);
 
-      await dom.moveTo(div1.children.first.nodeId, div2.nodeId);
+      await dom!.moveTo(div1.children!.first.nodeId, div2.nodeId);
 
       expect(div1.childNodeCount, 0);
       expect(div2.childNodeCount, 1);
@@ -71,56 +71,56 @@
     }, skip: 'google/webkit_inspection_protocol.dart/issues/52');
 
     test('Setting node value updates value', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
-      await dom.requestChildNodes(bodyNode.nodeId);
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
+      await dom!.requestChildNodes(bodyNode.nodeId);
 
-      Node div1 = bodyNode.children[0];
-      await dom.requestChildNodes(div1.nodeId);
+      Node div1 = bodyNode.children![0];
+      await dom!.requestChildNodes(div1.nodeId);
 
-      Node h1 = div1.children[0];
-      await dom.requestChildNodes(h1.nodeId);
+      Node h1 = div1.children![0];
+      await dom!.requestChildNodes(h1.nodeId);
 
-      Node text = h1.children[0];
+      Node text = h1.children![0];
 
       expect(text.nodeValue, 'test');
 
-      await dom.setNodeValue(text.nodeId, 'some new text');
+      await dom!.setNodeValue(text.nodeId, 'some new text');
 
       expect(text.nodeValue, 'some new text');
     });
 
     test('Adding attribute updates attributes', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
-      expect(bodyNode.attributes.containsKey('my-attr'), isFalse);
-      await dom.setAttributeValue(bodyNode.nodeId, 'my-attr', 'my-value');
-      expect(bodyNode.attributes['my-attr'], 'my-value');
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
+      expect(bodyNode.attributes!.containsKey('my-attr'), isFalse);
+      await dom!.setAttributeValue(bodyNode.nodeId, 'my-attr', 'my-value');
+      expect(bodyNode.attributes!['my-attr'], 'my-value');
     });
 
     test('Changing attribute updates attributes', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
-      expect(bodyNode.attributes['test-attr'], 'test-attr-value');
-      await dom.setAttributeValue(bodyNode.nodeId, 'test-attr', 'my-value');
-      expect(bodyNode.attributes['test-attr'], 'my-value');
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
+      expect(bodyNode.attributes!['test-attr'], 'test-attr-value');
+      await dom!.setAttributeValue(bodyNode.nodeId, 'test-attr', 'my-value');
+      expect(bodyNode.attributes!['test-attr'], 'my-value');
     });
 
     test('Removing attribute updates attributes', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
-      expect(bodyNode.attributes['test-attr'], 'test-attr-value');
-      await dom.removeAttribute(bodyNode.nodeId, 'test-attr');
-      expect(bodyNode.attributes.containsKey('test-attr'), isFalse);
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
+      expect(bodyNode.attributes!['test-attr'], 'test-attr-value');
+      await dom!.removeAttribute(bodyNode.nodeId, 'test-attr');
+      expect(bodyNode.attributes!.containsKey('test-attr'), isFalse);
     });
 
     test('refreshing resets document', () async {
-      var document1 = await dom.getDocument();
+      var document1 = await dom!.getDocument();
       await navigateToPage('dom_model_test.html');
-      var document2 = await dom.getDocument();
+      var document2 = await dom!.getDocument();
       expect(document2.nodeId, isNot(document1.nodeId));
     });
 
     test('getting attributes works', () async {
-      Node bodyNode = (await dom.getDocument()).children[1].children[1];
+      Node bodyNode = (await dom!.getDocument()).children![1].children![1];
       var attributes = bodyNode.attributes;
-      var getAttributes = await dom.getAttributes(bodyNode.nodeId);
+      var getAttributes = await dom!.getAttributes(bodyNode.nodeId);
 
       expect(getAttributes, attributes);
       expect(bodyNode.attributes, attributes);
diff --git a/test/runtime_test.dart b/test/runtime_test.dart
index 4f4b78a..33d61cf 100644
--- a/test/runtime_test.dart
+++ b/test/runtime_test.dart
@@ -13,7 +13,7 @@
 
 void main() {
   group('WipRuntime', () {
-    WipRuntime runtime;
+    WipRuntime? runtime;
     List<StreamSubscription> subs = [];
 
     setUp(() async {
@@ -21,46 +21,48 @@
     });
 
     tearDown(() async {
-      await runtime.disable();
+      await runtime?.disable();
       runtime = null;
 
       await closeConnection();
-      subs.forEach((s) => s.cancel());
+      for (var s in subs) {
+        s.cancel();
+      }
       subs.clear();
     });
 
     test('getIsolateId', () async {
-      await runtime.enable();
+      await runtime!.enable();
       await navigateToPage('runtime_test.html');
 
-      expect(await runtime.getIsolateId(), isNotEmpty);
+      expect(await runtime!.getIsolateId(), isNotEmpty);
     });
 
     test('getHeapUsage', () async {
-      await runtime.enable();
+      await runtime!.enable();
       await navigateToPage('runtime_test.html');
 
-      HeapUsage usage = await runtime.getHeapUsage();
+      HeapUsage usage = await runtime!.getHeapUsage();
 
       expect(usage.usedSize, greaterThan(0));
       expect(usage.totalSize, greaterThan(0));
     });
 
     test('evaluate', () async {
-      await runtime.enable();
+      await runtime!.enable();
       await navigateToPage('runtime_test.html');
 
-      RemoteObject result = await runtime.evaluate('1+1');
+      RemoteObject result = await runtime!.evaluate('1+1');
       expect(result.type, 'number');
       expect(result.value, 2);
     });
 
     test('callFunctionOn', () async {
-      await runtime.enable();
+      await runtime!.enable();
       await navigateToPage('runtime_test.html');
 
-      RemoteObject console = await runtime.evaluate('console');
-      RemoteObject result = await runtime.callFunctionOn(
+      RemoteObject console = await runtime!.evaluate('console');
+      RemoteObject result = await runtime!.callFunctionOn(
         '''
         function(msg) {
           console.log(msg);
@@ -77,12 +79,12 @@
     });
 
     test('getProperties', () async {
-      await runtime.enable();
+      await runtime!.enable();
       await navigateToPage('runtime_test.html');
 
-      RemoteObject console = await runtime.evaluate('console');
+      RemoteObject console = await runtime!.evaluate('console');
 
-      List<PropertyDescriptor> properties = await runtime.getProperties(
+      List<PropertyDescriptor> properties = await runtime!.getProperties(
         console,
         ownProperties: true,
       );
diff --git a/test/test_setup.dart b/test/test_setup.dart
index 5a5cfbe..857e445 100644
--- a/test/test_setup.dart
+++ b/test/test_setup.dart
@@ -10,26 +10,24 @@
 import 'package:webdriver/io.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
-Future<WipConnection> _wipConnection;
+Future<WipConnection>? _wipConnection;
 
 /// Returns a (cached) debugger connection to the first regular tab of
 /// the browser with remote debugger running at 'localhost:9222',
 Future<WipConnection> get wipConnection {
-  if (_wipConnection == null) {
-    _wipConnection = () async {
-      var debugPort = await _startWebDriver(await _startChromeDriver());
-      var chrome = new ChromeConnection('localhost', debugPort);
-      var tab = await chrome
-          .getTab((tab) => !tab.isBackgroundPage && !tab.isChromeExtension);
-      var connection = await tab.connect();
-      connection.onClose.listen((_) => _wipConnection = null);
-      return connection;
-    }();
-  }
-  return _wipConnection;
+  _wipConnection ??= () async {
+    var debugPort = await _startWebDriver(await _startChromeDriver());
+    var chrome = ChromeConnection('localhost', debugPort);
+    var tab = (await chrome
+        .getTab((tab) => !tab.isBackgroundPage && !tab.isChromeExtension))!;
+    var connection = await tab.connect();
+    connection.onClose.listen((_) => _wipConnection = null);
+    return connection;
+  }();
+  return _wipConnection!;
 }
 
-Process _chromeDriver;
+Process? _chromeDriver;
 
 /// Starts ChromeDriver and returns the listening port.
 Future<int> _startChromeDriver() async {
@@ -39,12 +37,12 @@
   await Future.delayed(const Duration(milliseconds: 25));
 
   try {
-    var _exeExt = Platform.isWindows ? '.exe' : '';
-    _chromeDriver = await Process.start('chromedriver$_exeExt',
+    var exeExt = Platform.isWindows ? '.exe' : '';
+    _chromeDriver = await Process.start('chromedriver$exeExt',
         ['--port=$chromeDriverPort', '--url-base=wd/hub']);
     // On windows this takes a while to boot up, wait for the first line
     // of stdout as a signal that it is ready.
-    await _chromeDriver.stdout
+    await _chromeDriver!.stdout
         .transform(utf8.decoder)
         .transform(const LineSplitter())
         .first;
@@ -55,7 +53,7 @@
   return chromeDriverPort;
 }
 
-WebDriver _webDriver;
+WebDriver? _webDriver;
 
 /// Starts WebDriver and returns the listening debug port.
 Future<int> _startWebDriver(int chromeDriverPort) async {
@@ -93,24 +91,22 @@
   return port;
 }
 
-var _testServerUri;
+Future<Uri>? _testServerUri;
 
 /// Ensures that an HTTP server serving files from 'test/data' has been
 /// started and navigates to to [page] using [wipConnection].
 /// Return [wipConnection].
 Future<WipConnection> navigateToPage(String page) async {
-  if (_testServerUri == null) {
-    _testServerUri = () async {
-      var receivePort = new ReceivePort();
-      await Isolate.spawn(_startHttpServer, receivePort.sendPort);
-      var port = await receivePort.first;
-      return new Uri.http('localhost:$port', '');
-    }();
-  }
+  _testServerUri ??= () async {
+    var receivePort = ReceivePort();
+    await Isolate.spawn(_startHttpServer, receivePort.sendPort);
+    var port = await receivePort.first;
+    return Uri.http('localhost:$port', '');
+  }();
   await (await wipConnection)
       .page
-      .navigate((await _testServerUri).resolve(page).toString());
-  await new Future.delayed(const Duration(seconds: 1));
+      .navigate((await _testServerUri)!.resolve(page).toString());
+  await Future.delayed(const Duration(seconds: 1));
   return wipConnection;
 }
 
diff --git a/tool/travis.sh b/tool/travis.sh
deleted file mode 100755
index 04cf687..0000000
--- a/tool/travis.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-
-# Copyright (c) 2014, Google Inc. Please see the AUTHORS file for details.
-# All rights reserved. Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# Fast fail the script on failures.
-set -e
-
-# Verify that the libraries are error free.
-pub global activate tuneup
-pub global run tuneup check
-
-# Install CHROMEDRIVER
-export CHROMEDRIVER_BINARY=/usr/bin/google-chrome
-export CHROMEDRIVER_OS=linux64
-export CHROME_LATEST_VERSION=$("$CHROMEDRIVER_BINARY" --version | cut -d' ' -f3 | cut -d'.' -f1)
-export CHROME_DRIVER_VERSION=$(wget -qO- https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROME_LATEST_VERSION)
-wget https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_$CHROMEDRIVER_OS.zip
-unzip "chromedriver_${CHROMEDRIVER_OS}.zip"
-export CHROMEDRIVER_ARGS=--no-sandbox
-export PATH=$PATH:$PWD
-
-# Run tests
-pub run test -j 1