Migrate to null safety (#17)

Named arguments are non-nullable with a default instead of a null
default. This means that code which was passing null through instead of
omitting the argument will be broken, even in unsound mode. I don't
expect any code was doing this, and this API is not one that should need
to be wrapped with argument forwarding.
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index 229bbf8..e8ed405 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -20,7 +20,7 @@
     strategy:
       fail-fast: false
       matrix:
-        sdk: [dev]
+        sdk: [2.12.0, dev]
     steps:
       - uses: actions/checkout@v2
       - uses: dart-lang/setup-dart@v0.3
@@ -31,14 +31,17 @@
         run: dart pub get
       - name: Check formatting
         run: dart format --output=none --set-exit-if-changed .
-        if: always() && steps.install.outcome == 'success'
+        if: matrix.sdk == 'dev' && steps.install.outcome == 'success'
       - name: Analyze code
         run: dart analyze --fatal-infos
-        if: always() && steps.install.outcome == 'success'
+        if: matrix.sdk == 'dev' && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: matrix.sdk != 'dev' && steps.install.outcome == 'success'
 
   # Run tests on a matrix consisting of two dimensions:
   # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
-  # 2. release channel: dev
+  # 2. release channel: oldest stable, dev
   test:
     needs: analyze
     runs-on: ${{ matrix.os }}
@@ -47,7 +50,7 @@
       matrix:
         # Add macos-latest and/or windows-latest if relevant for this package.
         os: [ubuntu-latest]
-        sdk: [dev]
+        sdk: [2.12.0, dev]
     steps:
       - uses: actions/checkout@v2
       - uses: dart-lang/setup-dart@v0.3
@@ -62,30 +65,3 @@
       - name: Run Chrome tests
         run: dart test --platform chrome
         if: always() && steps.install.outcome == 'success'
-
-  # Run tests on a matrix consisting of two dimensions:
-  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
-  # 2. release: 2.1.0
-  test-legacy-sdk:
-    needs: analyze
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        # Add macos-latest and/or windows-latest if relevant for this package.
-        os: [ubuntu-latest]
-        sdk: [2.1.0]
-    steps:
-      - uses: actions/checkout@v2
-      - uses: dart-lang/setup-dart@v0.3
-        with:
-          sdk: ${{ matrix.sdk }}
-      - id: install
-        name: Install dependencies
-        run: pub get
-      - name: Run VM tests
-        run: pub run test --platform vm
-        if: always() && steps.install.outcome == 'success'
-      - name: Run Chrome tests
-        run: pub run test --platform chrome
-        if: always() && steps.install.outcome == 'success'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30a8575..a4c55a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,9 @@
-## 0.1.2
+## 0.2.0-dev
 
+* Migrate to null safety.
 * Fix a number of lints affecting package maintenance score.
-* Update minimum Dart SDK to `2.1.0`.
+* **BREAKING** `null` may not be passed for most named arguments, instead the
+  argument must be omitted.
 
 ## 0.1.1+3
 
diff --git a/example/example.dart b/example/example.dart
index c024f93..863c36e 100644
--- a/example/example.dart
+++ b/example/example.dart
@@ -8,7 +8,7 @@
 Future<void> main() async {
   final client = RetryClient(http.Client());
   try {
-    print(await client.read('http://example.org'));
+    print(await client.read(Uri.http('example.org', '')));
   } finally {
     client.close();
   }
diff --git a/lib/http_retry.dart b/lib/http_retry.dart
index 825b5a2..f9298d1 100644
--- a/lib/http_retry.dart
+++ b/lib/http_retry.dart
@@ -7,7 +7,6 @@
 
 import 'package:async/async.dart';
 import 'package:http/http.dart';
-import 'package:pedantic/pedantic.dart';
 
 /// An HTTP client wrapper that automatically retries failing requests.
 class RetryClient extends BaseClient {
@@ -21,13 +20,13 @@
   final bool Function(BaseResponse) _when;
 
   /// The callback that determines whether a request when an error is thrown.
-  final bool Function(dynamic, StackTrace) _whenError;
+  final bool Function(Object, StackTrace) _whenError;
 
   /// The callback that determines how long to wait before retrying a request.
   final Duration Function(int) _delay;
 
   /// The callback to call to indicate that a request is being retried.
-  final void Function(BaseRequest, BaseResponse, int) _onRetry;
+  final void Function(BaseRequest, BaseResponse?, int)? _onRetry;
 
   /// Creates a client wrapping [_inner] that retries HTTP requests.
   ///
@@ -50,17 +49,15 @@
   /// error for which [whenError] returned `true`.
   RetryClient(
     this._inner, {
-    int retries,
-    bool Function(BaseResponse) when,
-    bool Function(Object, StackTrace) whenError,
-    Duration Function(int retryCount) delay,
-    void Function(BaseRequest, BaseResponse, int retryCount) onRetry,
-  })  : _retries = retries ?? 3,
-        _when = when ?? ((response) => response.statusCode == 503),
-        _whenError = whenError ?? ((_, __) => false),
-        _delay = delay ??
-            ((retryCount) =>
-                const Duration(milliseconds: 500) * math.pow(1.5, retryCount)),
+    int retries = 3,
+    bool Function(BaseResponse) when = _defaultWhen,
+    bool Function(Object, StackTrace) whenError = _defaultWhenError,
+    Duration Function(int retryCount) delay = _defaultDelay,
+    void Function(BaseRequest, BaseResponse?, int retryCount)? onRetry,
+  })  : _retries = retries,
+        _when = when,
+        _whenError = whenError,
+        _delay = delay,
         _onRetry = onRetry {
     RangeError.checkNotNegative(_retries, 'retries');
   }
@@ -74,9 +71,9 @@
   RetryClient.withDelays(
     Client inner,
     Iterable<Duration> delays, {
-    bool Function(BaseResponse) when,
-    bool Function(Object, StackTrace) whenError,
-    void Function(BaseRequest, BaseResponse, int retryCount) onRetry,
+    bool Function(BaseResponse) when = _defaultWhen,
+    bool Function(Object, StackTrace) whenError = _defaultWhenError,
+    void Function(BaseRequest, BaseResponse?, int retryCount)? onRetry,
   }) : this._withDelays(
           inner,
           delays.toList(),
@@ -88,9 +85,9 @@
   RetryClient._withDelays(
     Client inner,
     List<Duration> delays, {
-    bool Function(BaseResponse) when,
-    bool Function(Object, StackTrace) whenError,
-    void Function(BaseRequest, BaseResponse, int) onRetry,
+    required bool Function(BaseResponse) when,
+    required bool Function(Object, StackTrace) whenError,
+    required void Function(BaseRequest, BaseResponse?, int)? onRetry,
   }) : this(
           inner,
           retries: delays.length,
@@ -106,7 +103,7 @@
 
     var i = 0;
     for (;;) {
-      StreamedResponse response;
+      StreamedResponse? response;
       try {
         response = await _inner.send(_copyRequest(request, splitter.split()));
       } catch (error, stackTrace) {
@@ -118,11 +115,11 @@
 
         // Make sure the response stream is listened to so that we don't leave
         // dangling connections.
-        unawaited(response.stream.listen((_) {}).cancel()?.catchError((_) {}));
+        _unawaited(response.stream.listen((_) {}).cancel().catchError((_) {}));
       }
 
       await Future.delayed(_delay(i));
-      if (_onRetry != null) _onRetry(request, response, i);
+      _onRetry?.call(request, response, i);
       i++;
     }
   }
@@ -147,3 +144,12 @@
   @override
   void close() => _inner.close();
 }
+
+bool _defaultWhen(BaseResponse response) => response.statusCode == 503;
+
+bool _defaultWhenError(Object error, StackTrace stackTrace) => false;
+
+Duration _defaultDelay(int retryCount) =>
+    const Duration(milliseconds: 500) * math.pow(1.5, retryCount);
+
+void _unawaited(Future<void>? f) {}
diff --git a/pubspec.yaml b/pubspec.yaml
index 87e0bb1..9988670 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,18 +1,18 @@
 name: http_retry
-version: 0.1.2-dev
+version: 0.2.0-dev
 
 description: >-
   A wrapper for package:http clients that automatically retries requests
 homepage: https://github.com/dart-lang/http_retry
 
 environment:
-  sdk: '>=2.1.0 <3.0.0'
+  sdk: '>=2.12.0 <3.0.0'
 
 dependencies:
-  async: ^2.0.7
-  http: '>=0.11.0 <0.13.0'
-  pedantic: ^1.0.0
+  async: ^2.5.0
+  http: ^0.13.0
 
 dev_dependencies:
-  fake_async: ^1.0.0
-  test: ^1.2.0
+  fake_async: ^1.2.0
+  pedantic: ^1.10.0
+  test: ^1.16.0
diff --git a/test/http_retry_test.dart b/test/http_retry_test.dart
index bce4b73..f61b00f 100644
--- a/test/http_retry_test.dart
+++ b/test/http_retry_test.dart
@@ -13,7 +13,7 @@
     test('a request has a non-503 error code', () async {
       final client = RetryClient(
           MockClient(expectAsync1((_) async => Response('', 502), count: 1)));
-      final response = await client.get('http://example.org');
+      final response = await client.get(Uri.http('example.org', ''));
       expect(response.statusCode, equals(502));
     });
 
@@ -21,7 +21,7 @@
       final client = RetryClient(
           MockClient(expectAsync1((_) async => Response('', 503), count: 1)),
           when: (_) => false);
-      final response = await client.get('http://example.org');
+      final response = await client.get(Uri.http('example.org', ''));
       expect(response.statusCode, equals(503));
     });
 
@@ -29,7 +29,7 @@
       final client = RetryClient(
           MockClient(expectAsync1((_) async => Response('', 503), count: 1)),
           retries: 0);
-      final response = await client.get('http://example.org');
+      final response = await client.get(Uri.http('example.org', ''));
       expect(response.statusCode, equals(503));
     });
   });
@@ -43,7 +43,7 @@
         }, count: 2)),
         delay: (_) => Duration.zero);
 
-    final response = await client.get('http://example.org');
+    final response = await client.get(Uri.http('example.org', ''));
     expect(response.statusCode, equals(200));
   });
 
@@ -58,7 +58,7 @@
         when: (response) => response.headers['retry'] == 'true',
         delay: (_) => Duration.zero);
 
-    final response = await client.get('http://example.org');
+    final response = await client.get(Uri.http('example.org', ''));
     expect(response.headers, containsPair('retry', 'false'));
     expect(response.statusCode, equals(503));
   });
@@ -75,7 +75,7 @@
             error is StateError && error.message == 'oh no',
         delay: (_) => Duration.zero);
 
-    final response = await client.get('http://example.org');
+    final response = await client.get(Uri.http('example.org', ''));
     expect(response.statusCode, equals(200));
   });
 
@@ -85,7 +85,7 @@
         whenError: (error, _) => error == 'oh yeah',
         delay: (_) => Duration.zero);
 
-    expect(client.get('http://example.org'),
+    expect(client.get(Uri.http('example.org', '')),
         throwsA(isStateError.having((e) => e.message, 'message', 'oh no')));
   });
 
@@ -93,7 +93,7 @@
     final client = RetryClient(
         MockClient(expectAsync1((_) async => Response('', 503), count: 4)),
         delay: (_) => Duration.zero);
-    final response = await client.get('http://example.org');
+    final response = await client.get(Uri.http('example.org', ''));
     expect(response.statusCode, equals(503));
   });
 
@@ -102,7 +102,7 @@
         MockClient(expectAsync1((_) async => Response('', 503), count: 13)),
         retries: 12,
         delay: (_) => Duration.zero);
-    final response = await client.get('http://example.org');
+    final response = await client.get(Uri.http('example.org', ''));
     expect(response.statusCode, equals(503));
   });
 
@@ -124,7 +124,7 @@
         return Response('', 503);
       }, count: 4)));
 
-      expect(client.get('http://example.org'), completes);
+      expect(client.get(Uri.http('example.org', '')), completes);
       fake.elapse(const Duration(minutes: 10));
     });
   });
@@ -149,7 +149,7 @@
           }, count: 4)),
           delay: (requestCount) => Duration(seconds: requestCount));
 
-      expect(client.get('http://example.org'), completes);
+      expect(client.get(Uri.http('example.org', '')), completes);
       fake.elapse(const Duration(minutes: 10));
     });
   });
@@ -178,7 +178,7 @@
             Duration(seconds: 12)
           ]);
 
-      expect(client.get('http://example.org'), completes);
+      expect(client.get(Uri.http('example.org', '')), completes);
       fake.elapse(const Duration(minutes: 10));
     });
   });
@@ -190,12 +190,12 @@
         retries: 2,
         delay: (_) => Duration.zero,
         onRetry: expectAsync3((request, response, retryCount) {
-          expect(request.url, equals(Uri.parse('http://example.org')));
-          expect(response.statusCode, equals(503));
+          expect(request.url, equals(Uri.http('example.org', '')));
+          expect(response?.statusCode, equals(503));
           expect(retryCount, equals(count));
           count++;
         }, count: 2));
-    final response = await client.get('http://example.org');
+    final response = await client.get(Uri.http('example.org', ''));
     expect(response.statusCode, equals(503));
   });