diff --git a/README.md b/README.md index 65d2645..70a35a0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/example/lib/pages/custom_success_criteria.dart b/example/lib/pages/custom_success_criteria.dart index e8bc339..518bb77 100644 --- a/example/lib/pages/custom_success_criteria.dart +++ b/example/lib/pages/custom_success_criteria.dart @@ -12,12 +12,13 @@ class CustomSuccessCriteria extends StatefulWidget { class _CustomSuccessCriteriaState extends State { InternetStatus? _connectionStatus; + late InternetConnection _internetConnection; late StreamSubscription _subscription; @override void initState() { super.initState(); - _subscription = InternetConnection.createInstance( + _internetConnection = InternetConnection.createInstance( customCheckOptions: [ InternetCheckOption( uri: Uri.parse('https://img.shields.io/pub/'), @@ -27,7 +28,8 @@ class _CustomSuccessCriteriaState extends State { ), ], useDefaultOptions: false, - ).onStatusChange.listen((status) { + ); + _subscription = _internetConnection.onStatusChange.listen((status) { setState(() { _connectionStatus = status; }); @@ -37,6 +39,7 @@ class _CustomSuccessCriteriaState extends State { @override void dispose() { _subscription.cancel(); + _internetConnection.dispose(); super.dispose(); } diff --git a/example/lib/pages/custom_uris.dart b/example/lib/pages/custom_uris.dart index 5bbea0a..e34251d 100644 --- a/example/lib/pages/custom_uris.dart +++ b/example/lib/pages/custom_uris.dart @@ -12,12 +12,13 @@ class CustomURIs extends StatefulWidget { class _CustomURIsState extends State { InternetStatus? _connectionStatus; + late InternetConnection _internetConnection; late StreamSubscription _subscription; @override void initState() { super.initState(); - _subscription = InternetConnection.createInstance( + _internetConnection = InternetConnection.createInstance( customCheckOptions: [ InternetCheckOption( uri: Uri.parse('https://cloudflare.com/cdn-cgi/trace'), @@ -41,7 +42,8 @@ class _CustomURIsState extends State { InternetCheckOption(uri: Uri.parse('https://lenta.ru')), ], useDefaultOptions: false, - ).onStatusChange.listen((status) { + ); + _subscription = _internetConnection.onStatusChange.listen((status) { setState(() { _connectionStatus = status; }); @@ -51,6 +53,7 @@ class _CustomURIsState extends State { @override void dispose() { _subscription.cancel(); + _internetConnection.dispose(); super.dispose(); } diff --git a/example/lib/pages/listen_to_stream.dart b/example/lib/pages/listen_to_stream.dart index 8085ff1..b8f694d 100644 --- a/example/lib/pages/listen_to_stream.dart +++ b/example/lib/pages/listen_to_stream.dart @@ -13,14 +13,16 @@ class ListenToStream extends StatefulWidget { class _ListenToStreamState extends State { InternetStatus? _connectionStatus; + late InternetConnection _internetConnection; late StreamSubscription _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; }); @@ -30,6 +32,7 @@ class _ListenToStreamState extends State { @override void dispose() { _subscription.cancel(); + _internetConnection.dispose(); super.dispose(); } diff --git a/lib/src/internet_connection.dart b/lib/src/internet_connection.dart index c9e6051..ccb90c2 100644 --- a/lib/src/internet_connection.dart +++ b/lib/src/internet_connection.dart @@ -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? customCheckOptions, @@ -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 _handleStatusChangeCancel() async { + await _triggerSubscription?.cancel(); _triggerSubscription = null; _timerHandle?.cancel(); _timerHandle = null; @@ -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 dispose() async { + await _handleStatusChangeCancel(); + await _statusController.close(); + } } diff --git a/test/internet_connection_test.dart b/test/internet_connection_test.dart index 7dcc487..7083cf6 100644 --- a/test/internet_connection_test.dart +++ b/test/internet_connection_test.dart @@ -21,6 +21,8 @@ void main() { useDefaultOptions: false, ); expect(await checker.hasInternetAccess, false); + + await checker.dispose(); }); test('invokes responseStatusFn to determine success', () async { @@ -36,6 +38,8 @@ void main() { ); expect(await checker.hasInternetAccess, expectedStatus); + + await checker.dispose(); }); test('sends custom headers on request', () async { @@ -64,6 +68,8 @@ void main() { ); expect(await checker.hasInternetAccess, expectedStatus); + + await checker.dispose(); }); }); @@ -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>()); + + await checker.dispose(); }, ); @@ -111,6 +119,8 @@ void main() { final result = await checker.hasInternetAccess; expect(reachabilityCheckerCalled, true); expect(result, true); + + await checker.dispose(); }); }); @@ -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 { @@ -140,6 +152,8 @@ void main() { ], ); expect(await checker.hasInternetAccess, false); + + await checker.dispose(); }); }); @@ -152,8 +166,7 @@ void main() { counter++; return TestHttpClient.createResponse(statusCode: 200); }; - - final sub = InternetConnection.createInstance( + final checker = InternetConnection.createInstance( checkInterval: const Duration(milliseconds: 100), useDefaultOptions: false, customCheckOptions: [ @@ -161,7 +174,8 @@ void main() { uri: Uri.parse('https://www.example.com'), ), ], - ).onStatusChange.listen((_) {}); + ); + final sub = checker.onStatusChange.listen((_) {}); await Future.delayed(const Duration(milliseconds: 500)); @@ -169,6 +183,7 @@ void main() { expect(4 <= counter && counter <= 6, true); sub.cancel(); + await checker.dispose(); }); }); @@ -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: [ @@ -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(); }); }); @@ -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: [ @@ -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)); @@ -239,6 +255,7 @@ void main() { expect(4 <= counter && counter <= 6, true); sub.cancel(); + await checker.dispose(); }); }); }); @@ -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(); }); }); }