refactor(legacy): make guardDeviceCommand async with stale TTL refetch

Convert the shared command guard to an async check that refetches
/devices when the cached state is older than 30s, so the isDisconnect
flag reflects reality before a command runs. A TopSnackBar explains the
check only if the fetch takes longer than 400ms, avoiding noise on fast
responses. Update all 44 call sites to await the guard.
This commit is contained in:
2026-04-17 09:43:52 +02:00
parent ecbb6d1e76
commit 0b160758e2
35 changed files with 163 additions and 90 deletions

View File

@@ -39,7 +39,7 @@ class PedometerToggle extends ConsumerWidget {
value: enabled,
activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary),
onChanged: (value) async {
if (!guardDeviceCommand(context, ref)) return;
if (!await guardDeviceCommand(context, ref)) return;
final success = await ref
.read(activityMeterViewModelProvider.notifier)
.togglePedometer(enabled: value);

View File

@@ -97,8 +97,8 @@ class BackgroundImageScreen extends ConsumerWidget {
child: IconButton(
onPressed: state.isSaving
? null
: () {
if (!guardDeviceCommand(context, ref)) return;
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.uploadPhoto();
},
icon: Icon(
@@ -119,16 +119,16 @@ class BackgroundImageScreen extends ConsumerWidget {
? const Center(child: CircularProgressIndicator())
: state.photos.isEmpty
? _EmptyState(
onUpload: () {
if (!guardDeviceCommand(context, ref)) return;
onUpload: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.uploadPhoto();
},
primaryColor: primaryColor,
)
: _PhotoGrid(
state: state,
onPhotoTap: (id) {
if (!guardDeviceCommand(context, ref)) return;
onPhotoTap: (id) async {
if (!await guardDeviceCommand(context, ref)) return;
vm.setAsBackground(id);
},
primaryColor: primaryColor,

View File

@@ -58,8 +58,9 @@ class ContactsScreen extends ConsumerWidget {
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
if (!guardDeviceCommand(context, ref)) return;
onTap: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
if (state.contacts.length >= state.maxContacts) {
showTopSnackbar(
context,

View File

@@ -155,8 +155,8 @@ class DoNotDisturbScreen extends ConsumerWidget {
child: isSaving
? const Center(child: CircularProgressIndicator())
: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.save();
},
text: context.translate(I18n.doNotDisturbSave),

View File

@@ -110,7 +110,7 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
options: device.capabilities!.heartbeats!.options,
theme: theme,
onChanged: (frequency) async {
if (!guardDeviceCommand(context, ref)) return;
if (!await guardDeviceCommand(context, ref)) return;
final success = await vm.updateHeartRateFrequency(
frequency: frequency,
);
@@ -199,8 +199,8 @@ class _SaveSection extends ConsumerWidget {
child: PrimaryButton(
onPressed: isMeasuring
? null
: () {
if (!guardDeviceCommand(context, ref)) return;
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.measure();
},
text: isMeasuring ? '...' : context.translate(I18n.measure),

View File

@@ -90,8 +90,8 @@ class LocateDeviceDialog extends ConsumerWidget {
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 16)),
Expanded(
child: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.locateDevice();
},
text: context.translate(I18n.accept),

View File

@@ -152,8 +152,8 @@ class _TakePictureSection extends ConsumerWidget {
big: EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
child: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.takePicture();
},
text: context.translate(I18n.takePicture),

View File

@@ -33,8 +33,9 @@ class RemoteConnectionScreen extends ConsumerWidget {
children: [
if (cameraEnabled) ...[
_SectionButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
@@ -50,8 +51,9 @@ class RemoteConnectionScreen extends ConsumerWidget {
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 15)),
],
_SectionButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => Dialog(child: SpyCallDialog()),

View File

@@ -77,13 +77,13 @@ class SpyCallDialog extends ConsumerWidget {
),
),
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),
_PhoneSection(onSubmit: () {
if (!guardDeviceCommand(context, ref)) return;
_PhoneSection(onSubmit: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.call();
}),
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
_CallSection(onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
_CallSection(onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.call();
}),
],

View File

@@ -104,8 +104,9 @@ class _ScheduledActivitiesScreenState
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
if (!guardDeviceCommand(context, ref)) return;
onTap: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showActivityFormSheet(context, weekDay: _selectedWeekDay);
},
child: SizedBox(

View File

@@ -97,9 +97,10 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
});
}
void _submit() {
Future<void> _submit() async {
if (!_isFormValid) return;
if (!guardDeviceCommand(context, ref)) return;
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
final name = _nameController.text.trim();
final period = _buildPeriod();

View File

@@ -188,8 +188,9 @@ class _ActivityTimelineCard extends ConsumerWidget {
),
),
IconButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showActivityFormSheet(context, activity: activity);
},
icon: Icon(
@@ -201,8 +202,9 @@ class _ActivityTimelineCard extends ConsumerWidget {
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 6)),
),
IconButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showDialog(
context: context,
builder: (_) => Dialog(

View File

@@ -81,8 +81,8 @@ class VolumeControlScreen extends ConsumerWidget {
child: state.isLoading
? const Center(child: CircularProgressIndicator())
: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.submit();
},
text: context.translate(I18n.volumeSend),

View File

@@ -75,7 +75,7 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> {
Future<void> _submit() async {
if (!_canSave) return;
if (!guardDeviceCommand(context, ref)) return;
if (!await guardDeviceCommand(context, ref)) return;
final vm = ref.read(locationViewModelProvider.notifier);
final bool success;

View File

@@ -91,7 +91,7 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> {
Future<void> _submit() async {
if (!_canSave) return;
if (!guardDeviceCommand(context, ref)) return;
if (!await guardDeviceCommand(context, ref)) return;
final vm = ref.read(locationViewModelProvider.notifier);
final description = _descriptionController.text.trim();

View File

@@ -237,7 +237,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
}
Future<void> _updateFrequency(int frequency) async {
if (!guardDeviceCommand(context, ref)) return;
if (!await guardDeviceCommand(context, ref)) return;
final success = await ref
.read(locationViewModelProvider.notifier)
.updateLocationFrequency(frequency: frequency);
@@ -264,8 +264,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
}
}
void _confirmPlacement() {
if (!guardDeviceCommand(context, ref)) return;
Future<void> _confirmPlacement() async {
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
final center = _mapController.camera.center;
final mapState = ref.read(locationMapViewModelProvider);
@@ -289,8 +290,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
}
}
void _confirmRadius() {
if (!guardDeviceCommand(context, ref)) return;
Future<void> _confirmRadius() async {
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
final mapState = ref.read(locationMapViewModelProvider);
final point = mapState.previewPoint!;
final radius = mapState.previewRadius;
@@ -414,8 +416,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
}
}
void _onEditFrequentPlace(FrequentPlaceEntity fp) {
if (!guardDeviceCommand(context, ref)) return;
Future<void> _onEditFrequentPlace(FrequentPlaceEntity fp) async {
if (!await guardDeviceCommand(context, ref)) return;
if (!mounted) return;
_vm.clearSelectedFrequentPlace();
showNameInputSheet(
context,
@@ -673,12 +676,13 @@ class _LocationMapState extends ConsumerState<LocationMap>
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
onShareTap: _shareLocation,
onRefreshTap: () {
if (!guardDeviceCommand(context, ref)) return;
onRefreshTap: () async {
if (!await guardDeviceCommand(context, ref)) return;
unawaited(
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
);
widget.onRefreshPosition();
if (!mounted) return;
showTopSnackbar(
context,
message: context.translate(I18n.locationMapRefreshRequested),
@@ -686,9 +690,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
);
},
onCenterTap: _centerOnDevice,
onToggleFollow: () {
onToggleFollow: () async {
final willActivate = !mapState.isFollowing;
if (willActivate && !guardDeviceCommand(context, ref)) return;
if (willActivate && !await guardDeviceCommand(context, ref)) return;
_vm.toggleFollowing();
unawaited(
ref
@@ -698,6 +702,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
if (willActivate && widget.selectedPosition != null) {
_centerOnDevice();
}
if (!mounted) return;
showTopSnackbar(
context,
message: context.translate(
@@ -721,12 +726,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
child: GeofenceInfoCard(
geofence: mapState.selectedGeofence!,
onClose: _vm.clearSelectedGeofence,
onEdit: () {
if (!guardDeviceCommand(context, ref)) return;
onEdit: () async {
if (!await guardDeviceCommand(context, ref)) return;
_vm.startEditingGeofence(mapState.selectedGeofence!);
},
onDelete: () {
if (!guardDeviceCommand(context, ref)) return;
onDelete: () async {
if (!await guardDeviceCommand(context, ref)) return;
final id = mapState.selectedGeofence!.id;
_vm.clearSelectedGeofence();
ref
@@ -742,8 +747,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
frequentPlace: mapState.selectedFrequentPlace!,
onClose: _vm.clearSelectedFrequentPlace,
onEdit: () => _onEditFrequentPlace(mapState.selectedFrequentPlace!),
onDelete: () {
if (!guardDeviceCommand(context, ref)) return;
onDelete: () async {
if (!await guardDeviceCommand(context, ref)) return;
final id = mapState.selectedFrequentPlace!.id;
_vm.clearSelectedFrequentPlace();
ref

View File

@@ -92,8 +92,9 @@ class AlarmScreen extends ConsumerWidget {
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
_openForm(context, vm);
},
icon: Icon(
@@ -126,8 +127,8 @@ class AlarmScreen extends ConsumerWidget {
child: isSaving
? const Center(child: CircularProgressIndicator())
: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.save();
},
text: context.translate(I18n.alarmSave),

View File

@@ -81,8 +81,8 @@ class AlertsScreen extends ConsumerWidget {
child: PrimaryButton(
onPressed: state.isSaving
? null
: () {
if (!guardDeviceCommand(context, ref)) return;
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.save();
},
text: state.isSaving ? '...' : context.translate(I18n.save),

View File

@@ -99,8 +99,8 @@ class BatteryScreen extends ConsumerWidget {
activeTrackColor: primaryColor,
onChanged: state.isSaving
? null
: (value) {
if (!guardDeviceCommand(context, ref)) return;
: (value) async {
if (!await guardDeviceCommand(context, ref)) return;
vm.toggleNightMode(value);
},
),

View File

@@ -84,8 +84,9 @@ class BlockPhoneScreen extends ConsumerWidget {
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showAddContactSheet(context);
},
icon: Icon(
@@ -216,13 +217,14 @@ class _ContactList extends ConsumerWidget {
);
}
void _confirmDelete(
Future<void> _confirmDelete(
BuildContext context,
WidgetRef ref,
int index,
String name,
) {
if (!guardDeviceCommand(context, ref)) return;
) async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
final theme = ref.read(themePortProvider);
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);

View File

@@ -78,8 +78,8 @@ class DisableFunctionsScreen extends ConsumerWidget {
child: PrimaryButton(
onPressed: state.isSaving
? null
: () {
if (!guardDeviceCommand(context, ref)) return;
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.save();
},
text: state.isSaving ? '...' : context.translate(I18n.save),

View File

@@ -110,8 +110,8 @@ class _SaveSection extends ConsumerWidget {
child: isLoading
? const Center(child: CircularProgressIndicator())
: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.submit();
},
text: context.translate(I18n.save),

View File

@@ -51,8 +51,9 @@ class _OptionsSection extends ConsumerWidget {
title: context.translate(I18n.remoteTurnOff),
subtitle: context.translate(I18n.remoteTurnOffMessage),
icon: Icons.settings_power_outlined,
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => Dialog(
@@ -95,8 +96,9 @@ class _OptionsSection extends ConsumerWidget {
title: context.translate(I18n.remoteRestart),
subtitle: context.translate(I18n.remoteRestartMessage),
icon: Icons.refresh_outlined,
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => Dialog(
@@ -117,8 +119,9 @@ class _OptionsSection extends ConsumerWidget {
title: context.translate(I18n.remoteFactoryReset),
subtitle: context.translate(I18n.remoteFactoryResetMessage),
icon: Icons.restart_alt_outlined,
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => Dialog(

View File

@@ -177,8 +177,8 @@ class _SaveSection extends ConsumerWidget {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: PrimaryButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.submit();
},
text: context.translate(I18n.save),

View File

@@ -96,8 +96,8 @@ class TimezoneScreen extends ConsumerWidget {
child: PrimaryButton(
onPressed: state.isSaving
? null
: () {
if (!guardDeviceCommand(context, ref)) return;
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.save();
},
text: state.isSaving ? '...' : context.translate(I18n.save),

View File

@@ -94,8 +94,8 @@ class WifiSettingsScreen extends ConsumerWidget {
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () {
if (!guardDeviceCommand(context, ref)) return;
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.scanNetworks();
},
icon: Icon(
@@ -275,8 +275,13 @@ class _Body extends ConsumerWidget {
);
}
void _confirmDelete(BuildContext context, WidgetRef ref, dynamic network) {
if (!guardDeviceCommand(context, ref)) return;
Future<void> _confirmDelete(
BuildContext context,
WidgetRef ref,
dynamic network,
) async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
final theme = ref.read(themePortProvider);
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);

View File

@@ -1,12 +1,36 @@
import 'dart:async';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
bool guardDeviceCommand(BuildContext context, WidgetRef ref) {
final device = ref.read(selectedDeviceProvider).value;
const _deviceConnectionTtl = Duration(seconds: 30);
const _snackbarDelay = Duration(milliseconds: 400);
Future<bool> guardDeviceCommand(BuildContext context, WidgetRef ref) async {
final notifier = ref.read(legacyDevicesProvider.notifier);
if (notifier.isStale(_deviceConnectionTtl)) {
final fetchFuture = notifier.refreshIfStale(_deviceConnectionTtl);
final delayedSnackbar = Timer(_snackbarDelay, () {
if (!context.mounted) return;
showTopSnackbar(
context,
message: context.translate(I18n.checkingDeviceConnection),
type: MessageType.info,
);
});
await fetchFuture;
delayedSnackbar.cancel();
}
if (!context.mounted) return false;
final device = await ref.read(selectedDeviceProvider.future);
if (device == null || device.isDisconnected) {
if (!context.mounted) return false;
showTopSnackbar(
context,
message: context.translate(I18n.errorDeviceDisconnected),

View File

@@ -691,6 +691,7 @@
"errorContactsMax": "Maximale Kontaktanzahl für dieses Gerät erreicht",
"errorPositions": "Positionen konnten nicht geladen werden",
"errorDeviceDisconnected": "Die Uhr ist getrennt und kann keine Befehle empfangen",
"checkingDeviceConnection": "Verbindung des Geräts wird überprüft...",
"errorSosContactsMax": "Maximale SOS-Kontaktanzahl für dieses Gerät erreicht",
"customBackground": "Benutzerdefiniertes Hintergrundbild",
"backgroundImageDescription": "Legen Sie ein Foto als benutzerdefinierten Bildschirmschoner für das Gerät fest",

View File

@@ -859,6 +859,7 @@
"errorContactsMax": "Maximum contacts reached for this device",
"errorPositions": "Could not load positions",
"errorDeviceDisconnected": "The watch is disconnected and cannot receive commands",
"checkingDeviceConnection": "Checking if the device is connected...",
"errorSosContactsMax": "Maximum SOS contacts reached for this device",
"customBackground": "Custom background image",
"backgroundImageDescription": "Set a photo as a custom screensaver for the device",

View File

@@ -860,6 +860,7 @@
"errorContactsMax": "Se ha alcanzado el máximo de contactos para este dispositivo",
"errorPositions": "No se pudieron cargar las posiciones",
"errorDeviceDisconnected": "El reloj está desconectado y no puede recibir comandos",
"checkingDeviceConnection": "Revisando si el dispositivo está conectado...",
"errorSosContactsMax": "Se ha alcanzado el máximo de contactos SOS para este dispositivo",
"customBackground": "Fondo de pantalla personalizado",
"backgroundImageDescription": "Configura una foto como protector de pantalla exclusivo para el dispositivo",

View File

@@ -691,6 +691,7 @@
"errorContactsMax": "Nombre maximum de contacts atteint pour cet appareil",
"errorPositions": "Impossible de charger les positions",
"errorDeviceDisconnected": "La montre est déconnectée et ne peut pas recevoir de commandes",
"checkingDeviceConnection": "Vérification de la connexion de l'appareil...",
"errorSosContactsMax": "Nombre maximum de contacts SOS atteint pour cet appareil",
"customBackground": "Image de fond personnalisée",
"backgroundImageDescription": "Définissez une photo comme écran de veille personnalisé pour l'appareil",

View File

@@ -691,6 +691,7 @@
"errorContactsMax": "Numero massimo di contatti raggiunto per questo dispositivo",
"errorPositions": "Impossibile caricare le posizioni",
"errorDeviceDisconnected": "L'orologio è disconnesso e non può ricevere comandi",
"checkingDeviceConnection": "Verifica della connessione del dispositivo...",
"errorSosContactsMax": "Numero massimo di contatti SOS raggiunto per questo dispositivo",
"customBackground": "Immagine di sfondo personalizzata",
"backgroundImageDescription": "Imposta una foto come screensaver personalizzato per il dispositivo",

View File

@@ -691,6 +691,7 @@
"errorContactsMax": "Número máximo de contactos atingido para este dispositivo",
"errorPositions": "Não foi possível carregar as posições",
"errorDeviceDisconnected": "O relógio está desconectado e não pode receber comandos",
"checkingDeviceConnection": "A verificar se o dispositivo está conectado...",
"errorSosContactsMax": "Número máximo de contactos SOS atingido para este dispositivo",
"customBackground": "Imagem de fundo personalizada",
"backgroundImageDescription": "Defina uma foto como protetor de ecrã personalizado para o dispositivo",

View File

@@ -395,6 +395,7 @@ class I18n {
static const String errorContactsMax = 'errorContactsMax';
static const String errorPositions = 'errorPositions';
static const String errorDeviceDisconnected = 'errorDeviceDisconnected';
static const String checkingDeviceConnection = 'checkingDeviceConnection';
static const String errorSosContactsMax = 'errorSosContactsMax';
static const String errorContactsMin = 'errorContactsMin';
static const String errorDisableFunctions = 'errorDisableFunctions';

View File

@@ -2,9 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
DateTime? _lastFetchedAt;
@override
Future<List<DeviceEntity>> build() {
return ref.read(sharedDevicesRepositoryProvider).getDevices();
Future<List<DeviceEntity>> build() async {
final devices = await ref.read(sharedDevicesRepositoryProvider).getDevices();
_lastFetchedAt = DateTime.now();
return devices;
}
void removeDevice(String deviceId) {
@@ -30,6 +34,21 @@ class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
state = await AsyncValue.guard(
() => ref.read(sharedDevicesRepositoryProvider).getDevices(),
);
_lastFetchedAt = DateTime.now();
}
bool isStale(Duration ttl) {
final last = _lastFetchedAt;
return last == null || DateTime.now().difference(last) >= ttl;
}
Future<void> refreshIfStale(Duration ttl) async {
if (!isStale(ttl)) return;
final result = await AsyncValue.guard(
() => ref.read(sharedDevicesRepositoryProvider).getDevices(),
);
state = result;
_lastFetchedAt = DateTime.now();
}
}