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/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 new file mode 100644 index 0000000..50027e6 --- /dev/null +++ b/lib/src/refresh_when_network_available_extension.dart @@ -0,0 +1,60 @@ +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 +/// 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() { + var isNetworkAvailable = false; + const validResults = [ + ConnectivityResult.mobile, + ConnectivityResult.wifi, + ConnectivityResult.ethernet, + ConnectivityResult.vpn, + ConnectivityResult.other, + ]; + + listen(connectivityStreamProvider, (_, connectivityResults) { + connectivityResults.whenData((data) { + final currentlyAvailable = + data.any((result) => validResults.contains(result)); + if (currentlyAvailable && !isNetworkAvailable) { + isNetworkAvailable = true; + invalidateSelf(); + } else { + isNetworkAvailable = false; + } + }); + }); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7380b5e..e2c921c 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 aac4835..68bad6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dev_dependencies: very_good_analysis: ^5.1.0 dependencies: + connectivity_plus: ^6.0.3 flutter: sdk: flutter riverpod: ^2.0.0 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 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); + }); + }); +}