update for null safety (#145)

update for null safety
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4d33943..446e513 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -11,7 +11,7 @@
     runs-on: ubuntu-latest
 
     container:
-      image:  google/dart:beta
+      image:  google/dart:dev
 
     steps:
       - uses: actions/checkout@v2
diff --git a/.gitignore b/.gitignore
index 714afaf..b77fd3e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 # https://dart.dev/guides/libraries/private-files
 .packages
 .dart_tool/
+.idea/
 pubspec.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7a3c57..fdc54f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 4.0.0-nullsafety
+- Updated to support 2.12.0 and null safety.
+
 ## 3.4.2
 - A number of cleanups to improve the package health score.
 
diff --git a/example/example.dart b/example/example.dart
index bb7b345..46cc29b 100644
--- a/example/example.dart
+++ b/example/example.dart
@@ -9,26 +9,26 @@
 
 import 'package:usage/usage_html.dart';
 
-Analytics _analytics;
-String _lastUa;
+Analytics? _analytics;
+String? _lastUa;
 int _count = 0;
 
 void main() {
-  querySelector('#foo').onClick.listen((_) => _handleFoo());
-  querySelector('#bar').onClick.listen((_) => _handleBar());
-  querySelector('#page').onClick.listen((_) => _changePage());
+  querySelector('#foo')!.onClick.listen((_) => _handleFoo());
+  querySelector('#bar')!.onClick.listen((_) => _handleBar());
+  querySelector('#page')!.onClick.listen((_) => _changePage());
 }
 
-String _ua() => (querySelector('#ua') as InputElement).value.trim();
+String _ua() => (querySelector('#ua') as InputElement).value!.trim();
 
 Analytics getAnalytics() {
   if (_analytics == null || _lastUa != _ua()) {
     _lastUa = _ua();
-    _analytics = AnalyticsHtml(_lastUa, 'Test app', '1.0');
-    _analytics.sendScreenView(window.location.pathname);
+    _analytics = AnalyticsHtml(_lastUa!, 'Test app', '1.0');
+    _analytics!.sendScreenView(window.location.pathname!);
   }
 
-  return _analytics;
+  return _analytics!;
 }
 
 void _handleFoo() {
@@ -44,5 +44,5 @@
 void _changePage() {
   var analytics = getAnalytics();
   window.history.pushState(null, 'new page', '${++_count}.html');
-  analytics.sendScreenView(window.location.pathname);
+  analytics.sendScreenView(window.location.pathname!);
 }
diff --git a/lib/src/usage_impl.dart b/lib/src/usage_impl.dart
index fc87e95..07837d4 100644
--- a/lib/src/usage_impl.dart
+++ b/lib/src/usage_impl.dart
@@ -25,10 +25,9 @@
 class ThrottlingBucket {
   final int startingCount;
   int drops;
-  int _lastReplenish;
+  late int _lastReplenish;
 
-  ThrottlingBucket(this.startingCount) {
-    drops = startingCount;
+  ThrottlingBucket(this.startingCount) : drops = startingCount {
     _lastReplenish = DateTime.now().millisecondsSinceEpoch;
   }
 
@@ -61,9 +60,9 @@
   @override
   final String trackingId;
   @override
-  final String applicationName;
+  final String? applicationName;
   @override
-  final String applicationVersion;
+  final String? applicationVersion;
 
   final PersistentProperties properties;
   final PostHandler postHandler;
@@ -76,22 +75,20 @@
   @override
   AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut;
 
-  String _url;
+  late String _url;
 
   final StreamController<Map<String, dynamic>> _sendController =
       StreamController.broadcast(sync: true);
 
   AnalyticsImpl(this.trackingId, this.properties, this.postHandler,
-      {this.applicationName, this.applicationVersion, String analyticsUrl}) {
-    assert(trackingId != null);
-
+      {this.applicationName, this.applicationVersion, String? analyticsUrl}) {
     if (applicationName != null) setSessionValue('an', applicationName);
     if (applicationVersion != null) setSessionValue('av', applicationVersion);
 
     _url = analyticsUrl ?? _defaultAnalyticsUrl;
   }
 
-  bool _firstRun;
+  bool? _firstRun;
 
   @override
   bool get firstRun {
@@ -103,7 +100,7 @@
       }
     }
 
-    return _firstRun;
+    return _firstRun!;
   }
 
   @override
@@ -120,7 +117,7 @@
   }
 
   @override
-  Future sendScreenView(String viewName, {Map<String, String> parameters}) {
+  Future sendScreenView(String viewName, {Map<String, String>? parameters}) {
     var args = <String, dynamic>{'cd': viewName};
     if (parameters != null) {
       args.addAll(parameters);
@@ -130,7 +127,7 @@
 
   @override
   Future sendEvent(String category, String action,
-      {String label, int value, Map<String, String> parameters}) {
+      {String? label, int? value, Map<String, String>? parameters}) {
     var args = <String, dynamic>{'ec': category, 'ea': action};
     if (label != null) args['el'] = label;
     if (value != null) args['ev'] = value;
@@ -148,7 +145,7 @@
 
   @override
   Future sendTiming(String variableName, int time,
-      {String category, String label}) {
+      {String? category, String? label}) {
     var args = <String, dynamic>{'utv': variableName, 'utt': time};
     if (label != null) args['utl'] = label;
     if (category != null) args['utc'] = category;
@@ -157,12 +154,12 @@
 
   @override
   AnalyticsTimer startTimer(String variableName,
-      {String category, String label}) {
+      {String? category, String? label}) {
     return AnalyticsTimer(this, variableName, category: category, label: label);
   }
 
   @override
-  Future sendException(String description, {bool fatal}) {
+  Future sendException(String description, {bool? fatal}) {
     // We trim exceptions to a max length; google analytics will apply it's own
     // truncation, likely around 150 chars or so.
     const maxExceptionLength = 1000;
@@ -201,7 +198,7 @@
   Stream<Map<String, dynamic>> get onSend => _sendController.stream;
 
   @override
-  Future waitForLastPing({Duration timeout}) {
+  Future waitForLastPing({Duration? timeout}) {
     Future f = Future.wait(_futures).catchError((e) => null);
 
     if (timeout != null) {
@@ -268,6 +265,7 @@
   PersistentProperties(this.name);
 
   dynamic operator [](String key);
+
   void operator []=(String key, dynamic value);
 
   /// Re-read settings from the backing store. This may be a no-op on some
diff --git a/lib/src/usage_impl_html.dart b/lib/src/usage_impl_html.dart
index f2de075..2d8092d 100644
--- a/lib/src/usage_impl_html.dart
+++ b/lib/src/usage_impl_html.dart
@@ -15,38 +15,39 @@
 class AnalyticsHtml extends AnalyticsImpl {
   AnalyticsHtml(
       String trackingId, String applicationName, String applicationVersion,
-      {String analyticsUrl})
+      {String? analyticsUrl})
       : super(trackingId, HtmlPersistentProperties(applicationName),
             HtmlPostHandler(),
             applicationName: applicationName,
             applicationVersion: applicationVersion,
             analyticsUrl: analyticsUrl) {
-    var screenWidth = window.screen.width;
-    var screenHeight = window.screen.height;
+    var screenWidth = window.screen!.width;
+    var screenHeight = window.screen!.height;
 
     setSessionValue('sr', '${screenWidth}x$screenHeight');
-    setSessionValue('sd', '${window.screen.pixelDepth}-bits');
+    setSessionValue('sd', '${window.screen!.pixelDepth}-bits');
     setSessionValue('ul', window.navigator.language);
   }
 }
 
 typedef HttpRequestor = Future<HttpRequest> Function(String url,
-    {String method, dynamic sendData});
+    {String? method, dynamic sendData});
 
 class HtmlPostHandler extends PostHandler {
-  final HttpRequestor mockRequestor;
+  final HttpRequestor? mockRequestor;
 
   HtmlPostHandler({this.mockRequestor});
 
   @override
   Future sendPost(String url, Map<String, dynamic> parameters) {
-    var viewportWidth = document.documentElement.clientWidth;
-    var viewportHeight = document.documentElement.clientHeight;
+    var viewportWidth = document.documentElement!.clientWidth;
+    var viewportHeight = document.documentElement!.clientHeight;
 
     parameters['vp'] = '${viewportWidth}x$viewportHeight';
 
     var data = postEncode(parameters);
-    var requestor = mockRequestor ?? HttpRequest.request;
+    Future<HttpRequest> Function(String, {String method, dynamic sendData})
+        requestor = mockRequestor ?? HttpRequest.request;
     return requestor(url, method: 'POST', sendData: data).catchError((e) {
       // Catch errors that can happen during a request, but that we can't do
       // anything about, e.g. a missing internet connection.
@@ -58,7 +59,7 @@
 }
 
 class HtmlPersistentProperties extends PersistentProperties {
-  Map _map;
+  late Map _map;
 
   HtmlPersistentProperties(String name) : super(name) {
     var str = window.localStorage[name];
diff --git a/lib/src/usage_impl_io.dart b/lib/src/usage_impl_io.dart
index e942c89..eddc1ef 100644
--- a/lib/src/usage_impl_io.dart
+++ b/lib/src/usage_impl_io.dart
@@ -24,7 +24,7 @@
 class AnalyticsIO extends AnalyticsImpl {
   AnalyticsIO(
       String trackingId, String applicationName, String applicationVersion,
-      {String analyticsUrl, Directory documentDirectory})
+      {String? analyticsUrl, Directory? documentDirectory})
       : super(
             trackingId,
             IOPersistentProperties(applicationName,
@@ -75,9 +75,9 @@
 
 class IOPostHandler extends PostHandler {
   final String _userAgent;
-  final HttpClient mockClient;
+  final HttpClient? mockClient;
 
-  HttpClient _client;
+  HttpClient? _client;
 
   IOPostHandler({this.mockClient}) : _userAgent = _createUserAgent();
 
@@ -87,11 +87,11 @@
 
     if (_client == null) {
       _client = mockClient ?? HttpClient();
-      _client.userAgent = _userAgent;
+      _client!.userAgent = _userAgent;
     }
 
     try {
-      var req = await _client.postUrl(Uri.parse(url));
+      var req = await _client!.postUrl(Uri.parse(url));
       req.write(data);
       var response = await req.close();
       await response.drain();
@@ -108,10 +108,10 @@
 JsonEncoder _jsonEncoder = JsonEncoder.withIndent('  ');
 
 class IOPersistentProperties extends PersistentProperties {
-  File _file;
-  Map _map;
+  late File _file;
+  late Map _map;
 
-  IOPersistentProperties(String name, {String documentDirPath}) : super(name) {
+  IOPersistentProperties(String name, {String? documentDirPath}) : super(name) {
     var fileName = '.${name.replaceAll(' ', '_')}';
     documentDirPath ??= userHomeDir();
     _file = File(path.join(documentDirPath, fileName));
@@ -162,18 +162,15 @@
 
 /// Return the string for the platform's locale; return's `null` if the locale
 /// can't be determined.
-String getPlatformLocale() {
+String? getPlatformLocale() {
   var locale = Platform.localeName;
-  if (locale == null) return null;
 
-  if (locale != null) {
-    // Convert `en_US.UTF-8` to `en_US`.
-    var index = locale.indexOf('.');
-    if (index != -1) locale = locale.substring(0, index);
+  // Convert `en_US.UTF-8` to `en_US`.
+  var index = locale.indexOf('.');
+  if (index != -1) locale = locale.substring(0, index);
 
-    // Convert `en_US` to `en-us`.
-    locale = locale.replaceAll('_', '-').toLowerCase();
-  }
+  // Convert `en_US` to `en-us`.
+  locale = locale.replaceAll('_', '-').toLowerCase();
 
   return locale;
 }
diff --git a/lib/usage.dart b/lib/usage.dart
index 82a5b8f..4136d51 100644
--- a/lib/usage.dart
+++ b/lib/usage.dart
@@ -44,10 +44,10 @@
   String get trackingId;
 
   /// The application name.
-  String get applicationName;
+  String? get applicationName;
 
   /// The application version.
-  String get applicationVersion;
+  String? get applicationVersion;
 
   /// Is this the first time the tool has run?
   bool get firstRun;
@@ -72,7 +72,7 @@
   ///
   /// [parameters] can be any analytics key/value pair. Useful
   /// for custom dimensions, etc.
-  Future sendScreenView(String viewName, {Map<String, String> parameters});
+  Future sendScreenView(String viewName, {Map<String, String>? parameters});
 
   /// Sends an Event hit to Google Analytics. [label] specifies the event label.
   /// [value] specifies the event value. Values must be non-negative.
@@ -80,7 +80,7 @@
   /// [parameters] can be any analytics key/value pair. Useful
   /// for custom dimensions, etc.
   Future sendEvent(String category, String action,
-      {String label, int value, Map<String, String> parameters});
+      {String? label, int? value, Map<String, String>? parameters});
 
   /// Sends a Social hit to Google Analytics.
   ///
@@ -96,17 +96,17 @@
   /// milliseconds). [category] specifies the category of the timing. [label]
   /// specifies the label of the timing.
   Future sendTiming(String variableName, int time,
-      {String category, String label});
+      {String? category, String? label});
 
   /// Start a timer. The time won't be calculated, and the analytics information
   /// sent, until the [AnalyticsTimer.finish] method is called.
   AnalyticsTimer startTimer(String variableName,
-      {String category, String label});
+      {String? category, String? label});
 
   /// In order to avoid sending any personally identifying information, the
   /// [description] field must not contain the exception message. In addition,
   /// only the first 100 chars of the description will be sent.
-  Future sendException(String description, {bool fatal});
+  Future sendException(String description, {bool? fatal});
 
   /// Gets a session variable value.
   dynamic getSessionValue(String param);
@@ -136,7 +136,7 @@
   /// allows CLI apps to delay for a short time waiting for GA requests to
   /// complete, and then do something like call `dart:io`'s `exit()` explicitly
   /// themselves (or the [close] method below).
-  Future waitForLastPing({Duration timeout});
+  Future waitForLastPing({Duration? timeout});
 
   /// Free any used resources.
   ///
@@ -157,11 +157,11 @@
 class AnalyticsTimer {
   final Analytics analytics;
   final String variableName;
-  final String category;
-  final String label;
+  final String? category;
+  final String? label;
 
-  int _startMillis;
-  int _endMillis;
+  late int _startMillis;
+  int? _endMillis;
 
   AnalyticsTimer(this.analytics, this.variableName,
       {this.category, this.label}) {
@@ -172,7 +172,7 @@
     if (_endMillis == null) {
       return DateTime.now().millisecondsSinceEpoch - _startMillis;
     } else {
-      return _endMillis - _startMillis;
+      return _endMillis! - _startMillis;
     }
   }
 
@@ -220,7 +220,7 @@
   String get clientId => '00000000-0000-4000-0000-000000000000';
 
   @override
-  Future sendScreenView(String viewName, {Map<String, String> parameters}) {
+  Future sendScreenView(String viewName, {Map<String, String>? parameters}) {
     parameters ??= <String, String>{};
     parameters['viewName'] = viewName;
     return _log('screenView', parameters);
@@ -228,7 +228,7 @@
 
   @override
   Future sendEvent(String category, String action,
-      {String label, int value, Map<String, String> parameters}) {
+      {String? label, int? value, Map<String, String>? parameters}) {
     parameters ??= <String, String>{};
     return _log(
         'event',
@@ -242,7 +242,7 @@
 
   @override
   Future sendTiming(String variableName, int time,
-      {String category, String label}) {
+      {String? category, String? label}) {
     return _log('timing', {
       'variableName': variableName,
       'time': time,
@@ -253,12 +253,12 @@
 
   @override
   AnalyticsTimer startTimer(String variableName,
-      {String category, String label}) {
+      {String? category, String? label}) {
     return AnalyticsTimer(this, variableName, category: category, label: label);
   }
 
   @override
-  Future sendException(String description, {bool fatal}) =>
+  Future sendException(String description, {bool? fatal}) =>
       _log('exception', {'description': description, 'fatal': fatal});
 
   @override
@@ -271,7 +271,7 @@
   Stream<Map<String, dynamic>> get onSend => _sendController.stream;
 
   @override
-  Future waitForLastPing({Duration timeout}) => Future.value();
+  Future waitForLastPing({Duration? timeout}) => Future.value();
 
   @override
   void close() {}
@@ -300,7 +300,7 @@
   iter = iter.toList().reversed;
 
   for (var match in iter) {
-    var replacement = match.group(1);
+    var replacement = match.group(1)!;
     str =
         str.substring(0, match.start) + replacement + str.substring(match.end);
   }
diff --git a/pubspec.yaml b/pubspec.yaml
index 46fa921..257679b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,15 +3,15 @@
 # BSD-style license that can be found in the LICENSE file.
 
 name: usage
-version: 3.4.2
+version: 4.0.0-nullsafety
 description: A Google Analytics wrapper for command-line, web, and Flutter apps.
 homepage: https://github.com/dart-lang/usage
 
 environment:
-  sdk: '>=2.2.0 <3.0.0'
+  sdk: '>=2.12.0-0 <3.0.0'
 
 dependencies:
-  path: ^1.4.0
+  path: ^1.8.0-nullsafety
 
 dev_dependencies:
   pedantic: ^1.9.0
diff --git a/test/src/common.dart b/test/src/common.dart
index bcdabf6..304faf9 100644
--- a/test/src/common.dart
+++ b/test/src/common.dart
@@ -9,7 +9,7 @@
 import 'package:test/test.dart';
 import 'package:usage/src/usage_impl.dart';
 
-AnalyticsImplMock createMock({Map<String, dynamic> props}) =>
+AnalyticsImplMock createMock({Map<String, dynamic>? props}) =>
     AnalyticsImplMock('UA-0', props: props);
 
 void was(Map m, String type) => expect(m['t'], type);
@@ -17,10 +17,10 @@
 void hasnt(Map m, String key) => expect(m[key], isNull);
 
 class AnalyticsImplMock extends AnalyticsImpl {
-  MockProperties get mockProperties => properties;
-  MockPostHandler get mockPostHandler => postHandler;
+  MockProperties get mockProperties => properties as MockProperties;
+  MockPostHandler get mockPostHandler => postHandler as MockPostHandler;
 
-  AnalyticsImplMock(String trackingId, {Map<String, dynamic> props})
+  AnalyticsImplMock(String trackingId, {Map<String, dynamic>? props})
       : super(trackingId, MockProperties(props), MockPostHandler(),
             applicationName: 'Test App', applicationVersion: '0.1');
 
@@ -30,7 +30,7 @@
 class MockProperties extends PersistentProperties {
   Map<String, dynamic> props = {};
 
-  MockProperties([Map<String, dynamic> props]) : super('mock') {
+  MockProperties([Map<String, dynamic>? props]) : super('mock') {
     if (props != null) this.props.addAll(props);
   }
 
diff --git a/test/usage_impl_io_test.dart b/test/usage_impl_io_test.dart
index 2fc117b..8b15606 100644
--- a/test/usage_impl_io_test.dart
+++ b/test/usage_impl_io_test.dart
@@ -53,7 +53,7 @@
 
 class MockHttpClient implements HttpClient {
   @override
-  String userAgent;
+  String? userAgent;
   int sendCount = 0;
   int writeCount = 0;
   bool closed = false;
@@ -73,7 +73,7 @@
   MockHttpClientRequest(this.client);
 
   @override
-  void write(Object obj) {
+  void write(Object? obj) {
     client.writeCount++;
   }
 
@@ -93,7 +93,7 @@
   MockHttpClientResponse(this.client);
 
   @override
-  Future<E> drain<E>([E futureValue]) {
+  Future<E> drain<E>([E? futureValue]) {
     client.sendCount++;
     return Future.value();
   }
diff --git a/test/web_test.dart b/test/web_test.dart
index df2da92..eb0b107 100644
--- a/test/web_test.dart
+++ b/test/web_test.dart
@@ -59,7 +59,7 @@
 class MockRequestor {
   int sendCount = 0;
 
-  Future<HttpRequest> request(String url, {String method, sendData}) {
+  Future<HttpRequest> request(String url, {String? method, sendData}) {
     expect(url, isNotEmpty);
     expect(method, isNotEmpty);
     expect(sendData, isNotEmpty);