refactor: stabilize app bootstrap and polling
This commit is contained in:
@@ -14,7 +14,7 @@ import 'package:workmanager/workmanager.dart';
|
||||
/// Синглтон сервиса настроек
|
||||
final settingsServiceProvider = Provider((ref) => SettingsService());
|
||||
|
||||
/// API-клиент -- пересоздаётся при смене дома
|
||||
/// API-клиент текущего дома. Конфигурация меняется через init().
|
||||
final apiProvider = Provider((ref) => IgnisApi());
|
||||
|
||||
// ─── Текущий дом ─────────────────────────────────────────────
|
||||
@@ -43,8 +43,6 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
|
||||
await svc.setCurrentHomeId(home.id);
|
||||
state = home;
|
||||
await _initApi(home);
|
||||
// Перезагрузить группы для нового дома
|
||||
await ref.read(groupsProvider.notifier).initAndRefresh();
|
||||
}
|
||||
|
||||
/// Инициализировать API-клиент текущим домом
|
||||
@@ -234,9 +232,61 @@ final groupsProvider = NotifierProvider<GroupsNotifier, List<dynamic>>(
|
||||
() => GroupsNotifier(),
|
||||
);
|
||||
|
||||
enum GroupsLoadStatus { idle, loading, data, empty, error }
|
||||
|
||||
class GroupsLoadState {
|
||||
final GroupsLoadStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
const GroupsLoadState._(this.status, {this.errorMessage});
|
||||
|
||||
const GroupsLoadState.idle() : this._(GroupsLoadStatus.idle);
|
||||
|
||||
const GroupsLoadState.loading() : this._(GroupsLoadStatus.loading);
|
||||
|
||||
const GroupsLoadState.data() : this._(GroupsLoadStatus.data);
|
||||
|
||||
const GroupsLoadState.empty() : this._(GroupsLoadStatus.empty);
|
||||
|
||||
const GroupsLoadState.error(String message)
|
||||
: this._(GroupsLoadStatus.error, errorMessage: message);
|
||||
|
||||
bool get isLoading => status == GroupsLoadStatus.loading;
|
||||
|
||||
bool get hasError => status == GroupsLoadStatus.error;
|
||||
}
|
||||
|
||||
final groupsLoadStateProvider =
|
||||
NotifierProvider<GroupsLoadStateNotifier, GroupsLoadState>(
|
||||
GroupsLoadStateNotifier.new,
|
||||
);
|
||||
|
||||
class GroupsLoadStateNotifier extends Notifier<GroupsLoadState> {
|
||||
@override
|
||||
GroupsLoadState build() => const GroupsLoadState.idle();
|
||||
|
||||
void setIdle() => state = const GroupsLoadState.idle();
|
||||
|
||||
void setLoading() => state = const GroupsLoadState.loading();
|
||||
|
||||
void setData(List<dynamic> groups) {
|
||||
state = groups.isEmpty
|
||||
? const GroupsLoadState.empty()
|
||||
: const GroupsLoadState.data();
|
||||
}
|
||||
|
||||
void setError(Object error) =>
|
||||
state = GroupsLoadState.error(error.toString());
|
||||
}
|
||||
|
||||
class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
IgnisApi get _api => ref.read(apiProvider);
|
||||
Timer? _timer;
|
||||
bool _polling = false;
|
||||
bool _refreshInFlight = false;
|
||||
int? _refreshGeneration;
|
||||
int _pollingGeneration = 0;
|
||||
String? _pollingHomeId;
|
||||
|
||||
/// Блокировка обновления для группы после управления --
|
||||
/// чтобы UI не прыгал пока лампа ещё не ответила.
|
||||
@@ -248,7 +298,7 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
@override
|
||||
List<dynamic> build() {
|
||||
ref.onDispose(() {
|
||||
_timer?.cancel();
|
||||
_stopPolling(resetStatus: false);
|
||||
for (final t in _debounceTimers.values) {
|
||||
t.cancel();
|
||||
}
|
||||
@@ -256,21 +306,58 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Инициализация: настроить API и начать периодический опрос
|
||||
Future<void> initAndRefresh() async {
|
||||
/// Настроить API и начать периодический опрос для текущего дома.
|
||||
Future<void> startPolling() async {
|
||||
final home = ref.read(currentHomeProvider);
|
||||
if (home == null) return;
|
||||
if (home == null) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_polling && _pollingHomeId == home.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
_stopPolling(resetStatus: false);
|
||||
_polling = true;
|
||||
_pollingHomeId = home.id;
|
||||
final generation = ++_pollingGeneration;
|
||||
|
||||
final apiKey = await ref
|
||||
.read(settingsServiceProvider)
|
||||
.requireHomeApiKey(home.id);
|
||||
_api.init(home.url, apiKey);
|
||||
|
||||
await refresh();
|
||||
_timer?.cancel();
|
||||
if (!_isActiveGeneration(generation)) return;
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 10), (_) => refresh());
|
||||
}
|
||||
|
||||
void stopPolling() => _stopPolling();
|
||||
|
||||
void _stopPolling({bool resetStatus = true}) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_polling = false;
|
||||
_pollingHomeId = null;
|
||||
_pollingGeneration++;
|
||||
if (resetStatus) {
|
||||
ref.read(groupsLoadStateProvider.notifier).setIdle();
|
||||
}
|
||||
}
|
||||
|
||||
/// Полный опрос: загрузить группы + статус каждой
|
||||
Future<void> refresh() async {
|
||||
final generation = _pollingGeneration;
|
||||
if (_refreshInFlight && _refreshGeneration == generation) return;
|
||||
|
||||
_refreshInFlight = true;
|
||||
_refreshGeneration = generation;
|
||||
if (state.isEmpty) {
|
||||
ref.read(groupsLoadStateProvider.notifier).setLoading();
|
||||
}
|
||||
|
||||
try {
|
||||
final resGroups = await _api.getGroups();
|
||||
List<dynamic> rawList = [];
|
||||
@@ -338,12 +425,26 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
}),
|
||||
);
|
||||
|
||||
if (!_isActiveGeneration(generation)) return;
|
||||
|
||||
state = updatedList;
|
||||
ref.read(groupsLoadStateProvider.notifier).setData(updatedList);
|
||||
} catch (e) {
|
||||
debugPrint("Ошибка глобального опроса: $e");
|
||||
if (_isActiveGeneration(generation)) {
|
||||
ref.read(groupsLoadStateProvider.notifier).setError(e);
|
||||
}
|
||||
} finally {
|
||||
if (_refreshGeneration == generation) {
|
||||
_refreshInFlight = false;
|
||||
_refreshGeneration = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isActiveGeneration(int generation) =>
|
||||
generation == _pollingGeneration && _polling;
|
||||
|
||||
/// Установить блокировку на 5 секунд (чтобы UI не перетирал значения)
|
||||
void _setLock(String id) =>
|
||||
_lockUntil[id] = DateTime.now().add(const Duration(seconds: 5));
|
||||
@@ -711,7 +812,7 @@ class AuthInfoNotifier extends Notifier<Map<String, dynamic>?> {
|
||||
@override
|
||||
Map<String, dynamic>? build() => null;
|
||||
|
||||
Future<void> load() async {
|
||||
Future<void> load({bool failOnError = false}) async {
|
||||
try {
|
||||
final api = ref.read(apiProvider);
|
||||
final res = await api.getAuthMe();
|
||||
@@ -720,6 +821,7 @@ class AuthInfoNotifier extends Notifier<Map<String, dynamic>?> {
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Ошибка загрузки auth/me: $e");
|
||||
if (failOnError) rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user