Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@ final connection = InternetConnection.createInstance(
);
```

## Memory Management

If you create custom instances of `InternetConnection` using `createInstance()`, you should proactively free up resources when the instance is no longer needed to prevent memory leaks (e.g., lingering timers and unclosed stream controllers).

```dart
final customConnection = InternetConnection.createInstance();

// When done with the instance:
await customConnection.dispose();
```

> [!WARNING]
>
> **Never call `dispose()` on the global singleton (`InternetConnection()`).**
>
> The singleton is designed to live throughout the entire lifecycle of your application. Disposing of it will permanently close its internal streams and break any other parts of your app that rely on it.

### Custom HTTP Client Implementation

Integrate existing networking clients (like `dio`) to maintain consistent
Expand Down
7 changes: 5 additions & 2 deletions example/lib/pages/custom_success_criteria.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ class CustomSuccessCriteria extends StatefulWidget {

class _CustomSuccessCriteriaState extends State<CustomSuccessCriteria> {
InternetStatus? _connectionStatus;
late InternetConnection _internetConnection;
late StreamSubscription<InternetStatus> _subscription;

@override
void initState() {
super.initState();
_subscription = InternetConnection.createInstance(
_internetConnection = InternetConnection.createInstance(
customCheckOptions: [
InternetCheckOption(
uri: Uri.parse('https://img.shields.io/pub/'),
Expand All @@ -27,7 +28,8 @@ class _CustomSuccessCriteriaState extends State<CustomSuccessCriteria> {
),
],
useDefaultOptions: false,
).onStatusChange.listen((status) {
);
_subscription = _internetConnection.onStatusChange.listen((status) {
setState(() {
_connectionStatus = status;
});
Expand All @@ -37,6 +39,7 @@ class _CustomSuccessCriteriaState extends State<CustomSuccessCriteria> {
@override
void dispose() {
_subscription.cancel();
_internetConnection.dispose();
super.dispose();
}

Expand Down
7 changes: 5 additions & 2 deletions example/lib/pages/custom_uris.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ class CustomURIs extends StatefulWidget {

class _CustomURIsState extends State<CustomURIs> {
InternetStatus? _connectionStatus;
late InternetConnection _internetConnection;
late StreamSubscription<InternetStatus> _subscription;

@override
void initState() {
super.initState();
_subscription = InternetConnection.createInstance(
_internetConnection = InternetConnection.createInstance(
customCheckOptions: [
InternetCheckOption(
uri: Uri.parse('https://cloudflare.com/cdn-cgi/trace'),
Expand All @@ -41,7 +42,8 @@ class _CustomURIsState extends State<CustomURIs> {
InternetCheckOption(uri: Uri.parse('https://lenta.ru')),
],
useDefaultOptions: false,
).onStatusChange.listen((status) {
);
_subscription = _internetConnection.onStatusChange.listen((status) {
setState(() {
_connectionStatus = status;
});
Expand All @@ -51,6 +53,7 @@ class _CustomURIsState extends State<CustomURIs> {
@override
void dispose() {
_subscription.cancel();
_internetConnection.dispose();
super.dispose();
}

Expand Down
7 changes: 5 additions & 2 deletions example/lib/pages/listen_to_stream.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ class ListenToStream extends StatefulWidget {

class _ListenToStreamState extends State<ListenToStream> {
InternetStatus? _connectionStatus;
late InternetConnection _internetConnection;
late StreamSubscription<InternetStatus> _subscription;

@override
void initState() {
super.initState();
_subscription = InternetConnection.createInstance(
_internetConnection = InternetConnection.createInstance(
triggerStream: Connectivity().onConnectivityChanged,
).onStatusChange.listen((status) {
);
_subscription = _internetConnection.onStatusChange.listen((status) {
setState(() {
_connectionStatus = status;
});
Expand All @@ -30,6 +32,7 @@ class _ListenToStreamState extends State<ListenToStream> {
@override
void dispose() {
_subscription.cancel();
_internetConnection.dispose();
super.dispose();
}

Expand Down
22 changes: 20 additions & 2 deletions lib/src/internet_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class InternetConnection {
/// checking endpoint reachability. If provided, it will be used for all
/// connectivity checks instead of the default HTTP HEAD request
/// implementation.
///
/// Make sure to call [dispose] when this instance is no longer needed to free
/// up resources.
InternetConnection.createInstance({
Duration? checkInterval,
List<InternetCheckOption>? customCheckOptions,
Expand Down Expand Up @@ -274,8 +277,8 @@ class InternetConnection {
/// Handles cancellation of status change events.
///
/// Cancels the timer and resets the last status.
void _handleStatusChangeCancel() {
_triggerSubscription?.cancel();
Future<void> _handleStatusChangeCancel() async {
await _triggerSubscription?.cancel();
_triggerSubscription = null;
_timerHandle?.cancel();
_timerHandle = null;
Expand Down Expand Up @@ -306,4 +309,19 @@ class InternetConnection {
onError: (_, __) {},
);
}

/// Disposes of the resources used by this instance.
///
/// This method should be called when the instance created by
/// [InternetConnection.createInstance] is no longer needed to free up
/// resources.
///
/// ### Important:
/// Do not call this method on the singleton instance. Calling this method on
/// the singleton instance will affect all parts of the application that rely
/// on it. Use with caution.
Future<void> dispose() async {
await _handleStatusChangeCancel();
await _statusController.close();
}
}
39 changes: 29 additions & 10 deletions test/internet_connection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ void main() {
useDefaultOptions: false,
);
expect(await checker.hasInternetAccess, false);

await checker.dispose();
});

test('invokes responseStatusFn to determine success', () async {
Expand All @@ -36,6 +38,8 @@ void main() {
);

expect(await checker.hasInternetAccess, expectedStatus);

await checker.dispose();
});

test('sends custom headers on request', () async {
Expand Down Expand Up @@ -64,6 +68,8 @@ void main() {
);

expect(await checker.hasInternetAccess, expectedStatus);

await checker.dispose();
});
});

Expand All @@ -84,6 +90,8 @@ void main() {
// This inherently tests that a default client was created and used
// Successfully checking internet access means the internal client works
expect(checker.hasInternetAccess, isA<Future<bool>>());

await checker.dispose();
},
);

Expand Down Expand Up @@ -111,6 +119,8 @@ void main() {
final result = await checker.hasInternetAccess;
expect(reachabilityCheckerCalled, true);
expect(result, true);

await checker.dispose();
});
});

Expand All @@ -128,6 +138,8 @@ void main() {
useDefaultOptions: false,
);
expect(await checker.hasInternetAccess, true);

await checker.dispose();
});

test('returns false when any URI is unreachable', () async {
Expand All @@ -140,6 +152,8 @@ void main() {
],
);
expect(await checker.hasInternetAccess, false);

await checker.dispose();
});
});

Expand All @@ -152,23 +166,24 @@ void main() {
counter++;
return TestHttpClient.createResponse(statusCode: 200);
};

final sub = InternetConnection.createInstance(
final checker = InternetConnection.createInstance(
checkInterval: const Duration(milliseconds: 100),
useDefaultOptions: false,
customCheckOptions: [
InternetCheckOption(
uri: Uri.parse('https://www.example.com'),
),
],
).onStatusChange.listen((_) {});
);
final sub = checker.onStatusChange.listen((_) {});

await Future.delayed(const Duration(milliseconds: 500));

// Give it tiny error space.
expect(4 <= counter && counter <= 6, true);

sub.cancel();
await checker.dispose();
});
});

Expand All @@ -181,7 +196,7 @@ void main() {
return TestHttpClient.createResponse(statusCode: 200);
};

final instance = InternetConnection.createInstance(
final checker = InternetConnection.createInstance(
checkInterval: const Duration(milliseconds: 100),
useDefaultOptions: false,
customCheckOptions: [
Expand All @@ -191,20 +206,21 @@ void main() {
],
);

final sub = instance.onStatusChange.listen((_) {});
final sub = checker.onStatusChange.listen((_) {});

await Future.delayed(const Duration(milliseconds: 500));

// Give it tiny error space.
expect(4 <= counter && counter <= 6, true);

instance.setIntervalAndResetTimer(const Duration(milliseconds: 50));
checker.setIntervalAndResetTimer(const Duration(milliseconds: 50));

await Future.delayed(const Duration(milliseconds: 500));

expect(14 <= counter && counter <= 16, true);

sub.cancel();
await checker.dispose();
});
});

Expand All @@ -217,7 +233,7 @@ void main() {
return TestHttpClient.createResponse(statusCode: 200);
};

final instance = InternetConnection.createInstance(
final checker = InternetConnection.createInstance(
checkInterval: const Duration(milliseconds: 100),
useDefaultOptions: false,
customCheckOptions: [
Expand All @@ -227,10 +243,10 @@ void main() {
],
);

final sub = instance.onStatusChange.listen((_) {
final sub = checker.onStatusChange.listen((_) {
// Setting the same interval upon each emit should not
// result in any extra triggers.
instance.setIntervalAndResetTimer(instance.checkInterval);
checker.setIntervalAndResetTimer(checker.checkInterval);
});

await Future.delayed(const Duration(milliseconds: 500));
Expand All @@ -239,6 +255,7 @@ void main() {
expect(4 <= counter && counter <= 6, true);

sub.cancel();
await checker.dispose();
});
});
});
Expand All @@ -248,9 +265,11 @@ void main() {
expect(checker, InternetConnection());
});

test('createInstance constructor returns different instances', () {
test('createInstance constructor returns different instances', () async {
final checker = InternetConnection.createInstance();
expect(checker, isNot(InternetConnection.createInstance()));

await checker.dispose();
});
});
}