implemented connectivity debouncing and refetch throttling for loadable state UI components
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../extensions/date_time.dart';
|
||||
import '../loadable_state_indicators.dart';
|
||||
import 'loadable_state_event.dart';
|
||||
import 'loadable_state_state.dart';
|
||||
|
||||
@@ -13,48 +14,75 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
late StreamSubscription<List<ConnectivityResult>> _updateStream;
|
||||
void Function()? reFetch;
|
||||
|
||||
/// Last time [reFetch] was triggered by an [AppLifecycleState.resumed]
|
||||
/// event. Used to coalesce rapid foreground/background flips so we don't
|
||||
/// spam the network when the user briefly checks notifications.
|
||||
DateTime _lastResumeRefetch = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
static const Duration _refetchThrottle = Duration(seconds: 10);
|
||||
|
||||
/// Last time [reFetch] was triggered automatically (resume or reconnect).
|
||||
/// Used to coalesce rapid foreground/background flips so we don't spam the
|
||||
/// network when the user briefly checks notifications.
|
||||
DateTime _lastRefetch = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
/// Delays committing a transition *into* the offline state. On resume
|
||||
/// connectivity_plus frequently emits a transient `[none]` before the real
|
||||
/// result lands; debouncing it prevents the offline bar from flashing.
|
||||
Timer? _offlineDebounce;
|
||||
|
||||
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
|
||||
on<ConnectivityChanged>((event, emit) {
|
||||
emit(event.state);
|
||||
if (connectivityStatusKnown() && isConnected()) {
|
||||
if (reFetch == null) return;
|
||||
reFetch!();
|
||||
}
|
||||
if (connectivityStatusKnown() && isConnected()) _triggerRefetch();
|
||||
});
|
||||
|
||||
void emitConnectivity(List<ConnectivityResult> result) {
|
||||
Connectivity().checkConnectivity().then(_emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(
|
||||
_emitConnectivity,
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
void _emitConnectivity(List<ConnectivityResult> result) {
|
||||
_offlineDebounce?.cancel();
|
||||
// Commit a connected reading immediately (snappy recovery); only debounce
|
||||
// the drop to offline so a transient tick doesn't surface as offline.
|
||||
if (!result.contains(ConnectivityResult.none)) {
|
||||
// The initial checkConnectivity() future is not cancellable and may
|
||||
// resolve after the bloc was disposed, so guard against a closed sink.
|
||||
if (isClosed) return;
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
return;
|
||||
}
|
||||
_offlineDebounce = Timer(const Duration(milliseconds: 1200), () {
|
||||
if (isClosed) return;
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
});
|
||||
}
|
||||
|
||||
Connectivity().checkConnectivity().then(emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(
|
||||
emitConnectivity,
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
void _triggerRefetch() {
|
||||
final now = DateTime.now();
|
||||
if (!shouldRefetch(
|
||||
_lastRefetch,
|
||||
now,
|
||||
_refetchThrottle,
|
||||
hasReFetch: reFetch != null,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
_lastRefetch = now;
|
||||
reFetch!();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) {
|
||||
return;
|
||||
}
|
||||
_lastResumeRefetch = now;
|
||||
// Re-check connectivity so the resulting [ConnectivityChanged] handler
|
||||
// clears a stale error bar and triggers [reFetch] once reachable again.
|
||||
// Trigger the refetch synchronously so [RefetchStarted] clears any stale
|
||||
// error and enters the loading state before the first resume frame paints,
|
||||
// instead of only after the async connectivity round-trip below.
|
||||
_triggerRefetch();
|
||||
// Still re-check connectivity so the (debounced) offline bar reflects the
|
||||
// real network state after resume.
|
||||
unawaited(
|
||||
Connectivity().checkConnectivity().then((result) {
|
||||
if (isClosed) return;
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
_emitConnectivity(result);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -92,6 +120,7 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
@override
|
||||
Future<void> close() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_offlineDebounce?.cancel();
|
||||
_updateStream.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user