• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / flutter / 30854254088

03 Aug 2026 09:21PM UTC coverage: 40.895% (-15.0%) from 55.93%
30854254088

Pull #1260

github

web-flow
Merge d93794de5 into e3affdd4f
Pull Request #1260: WIP: health sync

9980 of 24404 relevant lines covered (40.89%)

1.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.44
/lib/core/network/network_provider.dart
1
/*
2
 * This file is part of wger Workout Manager <https://github.com/wger-project>.
3
 * Copyright (c) 2020 - 2026 wger Team
4
 *
5
 * wger Workout Manager is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Affero General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Affero General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Affero General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18

19
import 'dart:async';
20
import 'dart:io';
21

22
import 'package:connectivity_plus/connectivity_plus.dart';
23
import 'package:flutter/widgets.dart';
24
import 'package:http/http.dart' as http;
25
import 'package:logging/logging.dart';
26
import 'package:riverpod_annotation/riverpod_annotation.dart';
27
import 'package:wger/core/errors.dart';
28
import 'package:wger/core/network/wger_base.dart';
29

30
part 'network_provider.g.dart';
31

32
/// Returns whether the wger backend is actually reachable.
33
///
34
/// Given `probeUri` any HTTP response counts as reachable, only a
35
/// network-level error or timeout counts as offline. The request carries
36
/// `userAgent` so the probe is identifiable in server logs. When `probeUri` is
37
/// null (no server configured yet, e.g. on the login screen) it falls back to
38
/// a DNS lookup so there is still a sane online/offline signal.
39
///
40
/// Tests can swap this for a deterministic stub via `installFakeConnectivity()`
41
/// so the test runner doesn't make real network calls.
42
@visibleForTesting
43
Future<bool> Function(Uri? probeUri, String? userAgent, Duration timeout) reachabilityCheck =
5✔
44
    _defaultReachabilityCheck;
45

46
Future<bool> _defaultReachabilityCheck(
×
47
  Uri? probeUri,
48
  String? userAgent,
49
  Duration timeout,
50
) async {
51
  // No server configured yet: fall back to a generic internet check.
52
  if (probeUri == null) {
53
    try {
54
      final result = await InternetAddress.lookup('google.com').timeout(timeout);
×
55
      return result.isNotEmpty && result.first.rawAddress.isNotEmpty;
×
56
    } catch (_) {
57
      return false;
58
    }
59
  }
60

61
  final client = http.Client();
×
62
  try {
63
    // Any response (even 401/403) proves the server answered. HEAD keeps it
64
    // cheap and the version endpoint does not hit the database.
65
    await client
66
        .head(
×
67
          probeUri,
68
          headers: userAgent != null ? {HttpHeaders.userAgentHeader: userAgent} : null,
×
69
        )
70
        .timeout(timeout);
×
71
    return true;
72
  } catch (e) {
73
    if (isNetworkError(e)) {
×
74
      return false;
75
    }
76
    rethrow;
77
  } finally {
78
    client.close();
×
79
  }
80
}
81

82
/// Interval for the active backend re-probe (see [NetworkStatus]). Tests set
83
/// this to `null` to disable the periodic timer; a pending timer would
84
/// otherwise fail the test runner.
85
@visibleForTesting
86
Duration? networkProbeInterval = const Duration(seconds: 30);
6✔
87

88
@Riverpod(keepAlive: true)
89
class NetworkStatus extends _$NetworkStatus {
90
  final _logger = Logger('NetworkStatus');
91

92
  StreamSubscription<List<ConnectivityResult>>? _sub;
93
  Timer? _probeTimer;
94
  AppLifecycleListener? _lifecycleListener;
95

96
  @override
6✔
97
  bool build() {
98
    _logger.finer('Building NetworkStatus provider');
12✔
99
    _init();
6✔
100
    // Assume we're online until the first connectivity probe says otherwise,
101
    // this avoids e.g. flashing an "offline" state on app start
102
    return true;
103
  }
104

105
  void _init() {
6✔
106
    check(optimistic: true);
6✔
107

108
    _sub = Connectivity().onConnectivityChanged.listen((conn) async {
24✔
109
      await _update(conn, optimistic: true);
×
110
    });
111

112
    // A stale offline state from the background shouldn't stick until the
113
    // next timer tick, so re-check optimistically on resume.
114
    _lifecycleListener = AppLifecycleListener(onResume: () => check(optimistic: true));
12✔
115

116
    // Connectivity events only fire on adapter changes, so an active re-probe
117
    // is needed to notice a backend that goes down (or comes back) while the
118
    // network stays up.
119
    final probeInterval = networkProbeInterval;
6✔
120
    if (probeInterval != null) {
121
      _probeTimer = Timer.periodic(probeInterval, (_) => check());
4✔
122
    }
123

124
    ref.onDispose(() {
18✔
125
      _sub?.cancel();
12✔
126
      _probeTimer?.cancel();
7✔
127
      _lifecycleListener?.dispose();
12✔
128
    });
129
  }
130

131
  /// Re-checks connectivity and backend reachability, updates the state and
132
  /// returns it.
133
  ///
134
  /// With [optimistic] the state flips to online as soon as a network adapter
135
  /// is available and a failed probe only downgrades it afterwards; without it
136
  /// the state changes only once the probe has answered. The periodic re-probe
137
  /// stays pessimistic so a dead backend doesn't flash "online" every tick.
138
  Future<bool> check({
6✔
139
    Duration timeout = const Duration(seconds: 1),
140
    bool optimistic = false,
141
  }) async {
142
    final conn = await Connectivity().checkConnectivity();
12✔
143
    return _update(conn, timeout: timeout, optimistic: optimistic);
4✔
144
  }
145

146
  Future<bool> _update(
4✔
147
    List<ConnectivityResult> conn, {
148
    Duration timeout = const Duration(seconds: 1),
149
    bool optimistic = false,
150
  }) async {
151
    // Only short-circuit when there's clearly no network adapter at all. Any
152
    // other connectivity type (wifi, ethernet, mobile, vpn, other, ...) still
153
    // has to prove real reachability via the probe below. An empty list
154
    // counts as "no connection" too.
155
    if (conn.every((c) => c == ConnectivityResult.none)) {
12✔
156
      state = false;
×
157
      return false;
158
    }
159

160
    if (optimistic) {
161
      state = true;
4✔
162
    }
163

164
    final base = ref.read(wgerBaseProvider);
12✔
165
    final probeUri = base.serverUrl != null ? base.makeUrl('version') : null;
5✔
166
    final ok = await reachabilityCheck(probeUri, base.getAppNameHeaderValue(), timeout);
12✔
167
    state = ok;
4✔
168
    return ok;
169
  }
170
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc