From 76e111b630e67ad8824e3423fa37ab58aa7bdc3f Mon Sep 17 00:00:00 2001 From: Tanguy Mossion Date: Thu, 4 Jul 2024 13:27:02 +0200 Subject: [PATCH 1/5] feat(refreshWhenNetworkAvailable): add refreshWhenNetworkAvailable extension --- lib/riverpod_community_extensions.dart | 1 + ...resh_when_network_available_extension.dart | 62 +++++++++++++++++ pubspec.lock | 69 +++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 133 insertions(+) create mode 100644 lib/src/refresh_when_network_available_extension.dart diff --git a/lib/riverpod_community_extensions.dart b/lib/riverpod_community_extensions.dart index 3e2d378..02fb2dd 100644 --- a/lib/riverpod_community_extensions.dart +++ b/lib/riverpod_community_extensions.dart @@ -5,3 +5,4 @@ export 'src/cache_data_for_extension.dart'; export 'src/cache_for_extension.dart'; export 'src/debounce_extension.dart'; export 'src/refresh_extension.dart'; +export 'src/refresh_when_network_available_extension.dart'; diff --git a/lib/src/refresh_when_network_available_extension.dart b/lib/src/refresh_when_network_available_extension.dart new file mode 100644 index 0000000..63951a9 --- /dev/null +++ b/lib/src/refresh_when_network_available_extension.dart @@ -0,0 +1,62 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:riverpod/riverpod.dart'; + +/// Adds network-aware refresh functionality to AutoDisposeRef objects. +/// This extension uses the connectivity_plus package to listen for network +/// status changes and refreshes the provider when the network becomes +/// available. +/// +/// See [refreshWhenNetworkAvailable] +extension RefreshWhenNetworkAvailableExtension on AutoDisposeRef { + /// Refreshes the provider when the network becomes available after being + /// unavailable. + /// + /// This can be useful for scenarios where data needs to be fetched or + /// operations need to be performed only when network connectivity is + /// restored, such as synchronizing local data with a server. + /// + /// Example usages: + /// + /// without codegen: + /// ```dart + /// final myProvider = Provider.autoDispose((ref) { + /// ref.refreshWhenNetworkAvailable(); + /// return fetchData(); // Assume fetchData is a function that fetches data over the network. + /// }); + /// ``` + /// + /// with codegen: + /// ```dart + /// @riverpod + /// int myProvider(AutoDisposeRef ref) { + /// ref.refreshWhenNetworkAvailable(); + /// return fetchData(); + /// } + /// ``` + void refreshWhenNetworkAvailable() { + final connectivityStream = Connectivity().onConnectivityChanged; + + var isNetworkAvailable = false; + const validResults = [ + ConnectivityResult.mobile, + ConnectivityResult.wifi, + ConnectivityResult.ethernet, + ConnectivityResult.vpn, + ConnectivityResult.other, + ]; + + final connectivitySubscription = + connectivityStream.listen((connectivityResults) { + final currentlyAvailable = + connectivityResults.any((result) => validResults.contains(result)); + if (currentlyAvailable && !isNetworkAvailable) { + isNetworkAvailable = true; + invalidateSelf(); + } else { + isNetworkAvailable = false; + } + }); + + onDispose(connectivitySubscription.cancel); + } +} diff --git a/pubspec.lock b/pubspec.lock index 9371cad..c113cf3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8 + url: "https://pub.dev" + source: hosted + version: "6.0.3" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb + url: "https://pub.dev" + source: hosted + version: "2.0.0" conventional_commit: dependency: transitive description: @@ -89,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0+1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" fake_async: dependency: transitive description: @@ -97,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" file: dependency: transitive description: @@ -115,6 +147,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -243,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" path: dependency: transitive description: @@ -251,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -259,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -432,6 +493,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ce2579b..79c0b10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,4 +15,5 @@ dev_dependencies: dependencies: flutter: sdk: flutter + connectivity_plus: ^6.0.3 riverpod: ^2.0.0 From f24d7d021746e6546bbdd4cb4767a118067137e2 Mon Sep 17 00:00:00 2001 From: Tanguy Mossion Date: Thu, 22 Aug 2024 09:05:31 +0200 Subject: [PATCH 2/5] fix: Sort dependencies alphabetically --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 79c0b10..be3f3e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dev_dependencies: very_good_analysis: ^5.1.0 dependencies: + connectivity_plus: ^6.0.3 flutter: sdk: flutter - connectivity_plus: ^6.0.3 riverpod: ^2.0.0 From 141c2d2d9e0fd0490cf69714504cb24dbe54056a Mon Sep 17 00:00:00 2001 From: Tanguy Mossion Date: Fri, 5 Jul 2024 16:58:43 +0200 Subject: [PATCH 3/5] test(refreshWhenNetworkAvailable): check that the behavior is the one expected --- lib/src/connectivity_stream_provider.dart | 7 ++ ...resh_when_network_available_extension.dart | 26 +++---- ...when_network_available_extension_test.dart | 77 +++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 lib/src/connectivity_stream_provider.dart create mode 100644 test/src/refresh_when_network_available_extension_test.dart diff --git a/lib/src/connectivity_stream_provider.dart b/lib/src/connectivity_stream_provider.dart new file mode 100644 index 0000000..e4f1158 --- /dev/null +++ b/lib/src/connectivity_stream_provider.dart @@ -0,0 +1,7 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:riverpod/riverpod.dart'; + +/// A stream provider that listens to network connectivity changes. +final connectivityStreamProvider = StreamProvider( + (ref) => Connectivity().onConnectivityChanged.distinct(), +); diff --git a/lib/src/refresh_when_network_available_extension.dart b/lib/src/refresh_when_network_available_extension.dart index 63951a9..50027e6 100644 --- a/lib/src/refresh_when_network_available_extension.dart +++ b/lib/src/refresh_when_network_available_extension.dart @@ -1,5 +1,6 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_community_extensions/src/connectivity_stream_provider.dart'; /// Adds network-aware refresh functionality to AutoDisposeRef objects. /// This extension uses the connectivity_plus package to listen for network @@ -34,8 +35,6 @@ extension RefreshWhenNetworkAvailableExtension on AutoDisposeRef { /// } /// ``` void refreshWhenNetworkAvailable() { - final connectivityStream = Connectivity().onConnectivityChanged; - var isNetworkAvailable = false; const validResults = [ ConnectivityResult.mobile, @@ -45,18 +44,17 @@ extension RefreshWhenNetworkAvailableExtension on AutoDisposeRef { ConnectivityResult.other, ]; - final connectivitySubscription = - connectivityStream.listen((connectivityResults) { - final currentlyAvailable = - connectivityResults.any((result) => validResults.contains(result)); - if (currentlyAvailable && !isNetworkAvailable) { - isNetworkAvailable = true; - invalidateSelf(); - } else { - isNetworkAvailable = false; - } + listen(connectivityStreamProvider, (_, connectivityResults) { + connectivityResults.whenData((data) { + final currentlyAvailable = + data.any((result) => validResults.contains(result)); + if (currentlyAvailable && !isNetworkAvailable) { + isNetworkAvailable = true; + invalidateSelf(); + } else { + isNetworkAvailable = false; + } + }); }); - - onDispose(connectivitySubscription.cancel); } } diff --git a/test/src/refresh_when_network_available_extension_test.dart b/test/src/refresh_when_network_available_extension_test.dart new file mode 100644 index 0000000..fb671bd --- /dev/null +++ b/test/src/refresh_when_network_available_extension_test.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_community_extensions/src/connectivity_stream_provider.dart'; +import 'package:riverpod_community_extensions/src/refresh_when_network_available_extension.dart'; + +class MockConnectivity extends Mock implements Connectivity {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RefreshWhenNetworkAvailableExtension Tests', () { + late StreamController> connectivityController; + late ProviderContainer container; + late AutoDisposeProvider myProvider; + late Stream> connectivityStream; + + var numberOfFetchDataCalls = 0; + + int fetchData() { + numberOfFetchDataCalls++; + return 42; + } + + setUp(() { + numberOfFetchDataCalls = 0; + connectivityController = StreamController>(); + connectivityStream = connectivityController.stream; + + myProvider = Provider.autoDispose((ref) { + ref.refreshWhenNetworkAvailable(); + return fetchData(); + }); + + container = ProviderContainer( + overrides: [ + connectivityStreamProvider.overrideWith( + (ref) => connectivityStream, + ), + ], + )..listen(myProvider, (_, __) {}); + }); + + tearDown(() { + connectivityController.close(); + container.dispose(); + }); + + test('Should not refresh when connectivity does not change to available', + () async { + expect(numberOfFetchDataCalls, 1); + + // Simulate network being offline + connectivityController.add([ConnectivityResult.none]); + + // Wait for the potential refresh to happen + await Future.delayed(const Duration(seconds: 1)); + + expect(numberOfFetchDataCalls, 1); + }); + + test('Should refresh when connectivity changes to available', () async { + expect(numberOfFetchDataCalls, 1); + + // Simulate network being online + connectivityController.add([ConnectivityResult.wifi]); + + // Wait for the potential refresh to happen + await Future.delayed(const Duration(seconds: 1)); + + expect(numberOfFetchDataCalls, 2); + }); + }); +} From a4de85acab5f7350571dc72234a6bd4063e6eb00 Mon Sep 17 00:00:00 2001 From: Tanguy Mossion Date: Wed, 21 Aug 2024 13:58:30 +0200 Subject: [PATCH 4/5] test(ConnectivityStreamProvider): provides connectivity stream --- .../refresh_when_network_available_extension_test.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/src/refresh_when_network_available_extension_test.dart b/test/src/refresh_when_network_available_extension_test.dart index fb671bd..7878f96 100644 --- a/test/src/refresh_when_network_available_extension_test.dart +++ b/test/src/refresh_when_network_available_extension_test.dart @@ -74,4 +74,14 @@ void main() { expect(numberOfFetchDataCalls, 2); }); }); + + group('ConnectivityStreamProvider tests', () { + test('connectivityStreamProvider provides an AsyncValue', () { + final container = ProviderContainer(); + final providerState = container.read(connectivityStreamProvider); + + // Check that the initial state is AsyncLoading. + expect(providerState, isA>>()); + }); + }); } From 586a1f2f4d6281d65c0581feb33e14431795b10b Mon Sep 17 00:00:00 2001 From: Tanguy Mossion Date: Thu, 22 Aug 2024 09:36:49 +0200 Subject: [PATCH 5/5] refacto: ConnectivityStreamProvider tests in its own file --- .../src/connectivity_stream_provider_test.dart | 18 ++++++++++++++++++ ..._when_network_available_extension_test.dart | 10 ---------- 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 test/src/connectivity_stream_provider_test.dart diff --git a/test/src/connectivity_stream_provider_test.dart b/test/src/connectivity_stream_provider_test.dart new file mode 100644 index 0000000..bb50fa4 --- /dev/null +++ b/test/src/connectivity_stream_provider_test.dart @@ -0,0 +1,18 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_community_extensions/src/connectivity_stream_provider.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ConnectivityStreamProvider tests', () { + test('connectivityStreamProvider provides an AsyncValue', () { + final container = ProviderContainer(); + final providerState = container.read(connectivityStreamProvider); + + // Check that the initial state is AsyncLoading. + expect(providerState, isA>>()); + }); + }); +} diff --git a/test/src/refresh_when_network_available_extension_test.dart b/test/src/refresh_when_network_available_extension_test.dart index 7878f96..fb671bd 100644 --- a/test/src/refresh_when_network_available_extension_test.dart +++ b/test/src/refresh_when_network_available_extension_test.dart @@ -74,14 +74,4 @@ void main() { expect(numberOfFetchDataCalls, 2); }); }); - - group('ConnectivityStreamProvider tests', () { - test('connectivityStreamProvider provides an AsyncValue', () { - final container = ProviderContainer(); - final providerState = container.read(connectivityStreamProvider); - - // Check that the initial state is AsyncLoading. - expect(providerState, isA>>()); - }); - }); }