refactor(offline): single connectivity subscription in OfflineModeProvider

This commit is contained in:
edde746
2026-06-12 14:18:49 +02:00
parent 5e4449247b
commit acdae74587
2 changed files with 56 additions and 34 deletions
+31 -33
View File
@@ -444,7 +444,11 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
late final OfflineWatchSyncService _offlineWatchSyncService;
late final AppLifecycleListener _appLifecycleListener;
StreamSubscription<WatchStateEvent>? _watchStateSubscription;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
/// WiFi-reconnect sync trigger, listening on [OfflineModeProvider] — the
/// app's single connectivity subscription lives there.
VoidCallback? _connectivitySyncListener;
OfflineModeProvider? _connectivitySyncProvider;
Timer? _syncDebounce;
final Set<String> _pendingSyncKeys = <String>{};
bool _isAutoDeleteRunning = false;
@@ -507,7 +511,7 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
_syncDebounce?.cancel();
await _watchStateSubscription?.cancel();
await _connectivitySubscription?.cancel();
_removeConnectivitySyncListener();
_memoryCheckTimer?.cancel();
_downloadManager.dispose();
@@ -528,7 +532,7 @@ class _MainAppState extends State<MainApp> 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<MainApp> 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<void> 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<ConnectivityResult> 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<MainApp> 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.
+25 -1
View File
@@ -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<ConnectivityResult> _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;