From acdae745875404fa21b1b613c78dc5ab0d98783e Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:18:49 +0200 Subject: [PATCH] refactor(offline): single connectivity subscription in OfflineModeProvider --- lib/main.dart | 64 ++++++++++++------------ lib/providers/offline_mode_provider.dart | 26 +++++++++- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c31f163d..b753113e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -444,7 +444,11 @@ class _MainAppState extends State with WidgetsBindingObserver { late final OfflineWatchSyncService _offlineWatchSyncService; late final AppLifecycleListener _appLifecycleListener; StreamSubscription? _watchStateSubscription; - StreamSubscription>? _connectivitySubscription; + + /// WiFi-reconnect sync trigger, listening on [OfflineModeProvider] — the + /// app's single connectivity subscription lives there. + VoidCallback? _connectivitySyncListener; + OfflineModeProvider? _connectivitySyncProvider; Timer? _syncDebounce; final Set _pendingSyncKeys = {}; bool _isAutoDeleteRunning = false; @@ -507,7 +511,7 @@ class _MainAppState extends State with WidgetsBindingObserver { _syncDebounce?.cancel(); await _watchStateSubscription?.cancel(); - await _connectivitySubscription?.cancel(); + _removeConnectivitySyncListener(); _memoryCheckTimer?.cancel(); _downloadManager.dispose(); @@ -528,7 +532,7 @@ class _MainAppState extends State with WidgetsBindingObserver { void dispose() { _syncDebounce?.cancel(); _watchStateSubscription?.cancel(); - _connectivitySubscription?.cancel(); + _removeConnectivitySyncListener(); _memoryCheckTimer?.cancel(); _appLifecycleListener.dispose(); if (!_shutdownStarted) { @@ -552,38 +556,32 @@ class _MainAppState extends State with WidgetsBindingObserver { } /// Fires [_autoDeleteAndSync] on each WiFi/Ethernet reconnect so rules run - /// as soon as the device is back online. Rapid flapping is bounded by the - /// executor's cooldown. - void _startConnectivitySyncTrigger(DownloadProvider downloadProvider) { - Future setup() async { - try { - final initial = await Connectivity().checkConnectivity(); - _lastConnectivityWasWifi = _hasWifiOrEthernet(initial); - } catch (e) { - appLogger.w('Initial connectivity read failed, defaulting to false: $e'); - _lastConnectivityWasWifi = false; + /// as soon as the device is back online. Listens on [OfflineModeProvider], + /// which owns the app's single connectivity subscription and notifies on + /// connection-type changes. Rapid flapping is bounded by the executor's + /// cooldown. + void _startConnectivitySyncTrigger(DownloadProvider downloadProvider, OfflineModeProvider offlineModeProvider) { + _removeConnectivitySyncListener(); + _lastConnectivityWasWifi = offlineModeProvider.hasWifiOrEthernet; + _connectivitySyncProvider = offlineModeProvider; + _connectivitySyncListener = () { + final hasWifi = offlineModeProvider.hasWifiOrEthernet; + final transitioned = hasWifi && !_lastConnectivityWasWifi; + _lastConnectivityWasWifi = hasWifi; + if (transitioned) { + appLogger.d('Connectivity moved onto WiFi/Ethernet — triggering sync pass'); + _autoDeleteAndSync(downloadProvider); } - - try { - _connectivitySubscription = Connectivity().onConnectivityChanged.listen((results) { - final hasWifi = _hasWifiOrEthernet(results); - final transitioned = hasWifi && !_lastConnectivityWasWifi; - _lastConnectivityWasWifi = hasWifi; - if (transitioned) { - appLogger.d('Connectivity moved onto WiFi/Ethernet — triggering sync pass'); - _autoDeleteAndSync(downloadProvider); - } - }); - } catch (e) { - appLogger.w('Could not subscribe to connectivity changes: $e'); - } - } - - setup(); + }; + offlineModeProvider.addListener(_connectivitySyncListener!); } - static bool _hasWifiOrEthernet(List results) => - results.contains(ConnectivityResult.wifi) || results.contains(ConnectivityResult.ethernet); + void _removeConnectivitySyncListener() { + final listener = _connectivitySyncListener; + if (listener != null) _connectivitySyncProvider?.removeListener(listener); + _connectivitySyncListener = null; + _connectivitySyncProvider = null; + } /// Run auto-delete (if enabled) and then a sync-rule pass. /// @@ -820,7 +818,7 @@ class _MainAppState extends State with WidgetsBindingObserver { }); }); - _startConnectivitySyncTrigger(downloadProvider); + _startConnectivitySyncTrigger(downloadProvider, offlineModeProvider); // Thread the offline flag into services so queue/resume paths can // short-circuit instead of hitting the network and failing. diff --git a/lib/providers/offline_mode_provider.dart b/lib/providers/offline_mode_provider.dart index aadc17e2..c9541bc8 100644 --- a/lib/providers/offline_mode_provider.dart +++ b/lib/providers/offline_mode_provider.dart @@ -28,6 +28,18 @@ class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMi bool _lastOfflineState = false; bool _isInitialized = false; + /// Latest raw connectivity results. This provider owns the app's single + /// `Connectivity()` subscription; consumers needing the connection *type* + /// (e.g. the WiFi-reconnect sync trigger in main.dart) read it from here + /// instead of subscribing themselves. + List _lastConnectivityResults = const []; + bool _lastWifiOrEthernetState = false; + + /// Whether the current connection is WiFi or Ethernet (unmetered-ish). + bool get hasWifiOrEthernet => + _lastConnectivityResults.contains(ConnectivityResult.wifi) || + _lastConnectivityResults.contains(ConnectivityResult.ethernet); + /// True once [MultiServerManager] has emitted its first server-status /// snapshot. Until then we don't actually know whether any server is /// online — the binder hasn't finished its first connect yet — so we @@ -98,6 +110,8 @@ class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMi const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other], ); + _lastConnectivityResults = connectivityResult; + _lastWifiOrEthernetState = hasWifiOrEthernet; _hasNetworkConnection = !connectivityResult.contains(ConnectivityResult.none); } catch (e) { // connectivity_plus can throw PlatformException on Windows (NetworkManager::StartListen) @@ -139,8 +153,18 @@ class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMi () { _connectivitySubscription = Connectivity().onConnectivityChanged.listen( (results) { + _lastConnectivityResults = results; _hasNetworkConnection = !results.contains(ConnectivityResult.none); - _notifyIfOfflineChanged(); + // Notify on connection-type changes too (WiFi <-> cellular), not + // just offline flips — type consumers listen through this provider. + final wifiNow = hasWifiOrEthernet; + if (wifiNow != _lastWifiOrEthernetState) { + _lastWifiOrEthernetState = wifiNow; + _lastOfflineState = isOffline; + safeNotifyListeners(); + } else { + _notifyIfOfflineChanged(); + } }, onError: (e) { _hasNetworkConnection = true;