refactor: stabilize app bootstrap and polling
This commit is contained in:
108
lib/app/app_bootstrap.dart
Normal file
108
lib/app/app_bootstrap.dart
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../providers/providers.dart';
|
||||||
|
|
||||||
|
enum AppBootstrapStatus {
|
||||||
|
bootstrapping,
|
||||||
|
noHomes,
|
||||||
|
ready,
|
||||||
|
authFailed,
|
||||||
|
networkFailed,
|
||||||
|
invalidConfig,
|
||||||
|
failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppBootstrapState {
|
||||||
|
final AppBootstrapStatus status;
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
const AppBootstrapState._(this.status, {this.message});
|
||||||
|
|
||||||
|
const AppBootstrapState.bootstrapping()
|
||||||
|
: this._(AppBootstrapStatus.bootstrapping);
|
||||||
|
|
||||||
|
const AppBootstrapState.noHomes() : this._(AppBootstrapStatus.noHomes);
|
||||||
|
|
||||||
|
const AppBootstrapState.ready() : this._(AppBootstrapStatus.ready);
|
||||||
|
|
||||||
|
const AppBootstrapState.authFailed(String message)
|
||||||
|
: this._(AppBootstrapStatus.authFailed, message: message);
|
||||||
|
|
||||||
|
const AppBootstrapState.networkFailed(String message)
|
||||||
|
: this._(AppBootstrapStatus.networkFailed, message: message);
|
||||||
|
|
||||||
|
const AppBootstrapState.invalidConfig(String message)
|
||||||
|
: this._(AppBootstrapStatus.invalidConfig, message: message);
|
||||||
|
|
||||||
|
const AppBootstrapState.failed(String message)
|
||||||
|
: this._(AppBootstrapStatus.failed, message: message);
|
||||||
|
|
||||||
|
bool get isBootstrapping => status == AppBootstrapStatus.bootstrapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
final appBootstrapProvider =
|
||||||
|
NotifierProvider<AppBootstrapNotifier, AppBootstrapState>(
|
||||||
|
AppBootstrapNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AppBootstrapNotifier extends Notifier<AppBootstrapState> {
|
||||||
|
@override
|
||||||
|
AppBootstrapState build() => const AppBootstrapState.bootstrapping();
|
||||||
|
|
||||||
|
Future<void> bootstrap() async {
|
||||||
|
state = const AppBootstrapState.bootstrapping();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(homesProvider.notifier).load();
|
||||||
|
await ref.read(currentHomeProvider.notifier).load();
|
||||||
|
|
||||||
|
final home = ref.read(currentHomeProvider);
|
||||||
|
if (home == null) {
|
||||||
|
state = const AppBootstrapState.noHomes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref.read(authInfoProvider.notifier).load(failOnError: true);
|
||||||
|
await syncGeofenceTask(ref.read(homesProvider));
|
||||||
|
|
||||||
|
state = const AppBootstrapState.ready();
|
||||||
|
} catch (e) {
|
||||||
|
state = _failureState(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBootstrapState _failureState(Object error) {
|
||||||
|
if (error is StateError || error is FormatException) {
|
||||||
|
return AppBootstrapState.invalidConfig(error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is DioException) {
|
||||||
|
final statusCode = error.response?.statusCode;
|
||||||
|
if (statusCode == 401 || statusCode == 403) {
|
||||||
|
return AppBootstrapState.authFailed(
|
||||||
|
'API key не прошёл авторизацию ($statusCode)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isNetworkFailure(error)) {
|
||||||
|
return AppBootstrapState.networkFailed(
|
||||||
|
'Backend недоступен: ${error.message ?? error.type.name}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppBootstrapState.failed(error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isNetworkFailure(DioException error) {
|
||||||
|
return switch (error.type) {
|
||||||
|
DioExceptionType.connectionTimeout ||
|
||||||
|
DioExceptionType.sendTimeout ||
|
||||||
|
DioExceptionType.receiveTimeout ||
|
||||||
|
DioExceptionType.connectionError ||
|
||||||
|
DioExceptionType.unknown => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
import 'providers/providers.dart';
|
import 'app/app_bootstrap.dart';
|
||||||
import 'screens/homes_screen.dart';
|
import 'screens/homes_screen.dart';
|
||||||
import 'screens/remote_screen.dart';
|
import 'screens/remote_screen.dart';
|
||||||
import 'services/geofence_worker.dart';
|
import 'services/geofence_worker.dart';
|
||||||
@@ -78,53 +78,83 @@ class _MainGateState extends ConsumerState<MainGate> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_bootstrap();
|
Future.microtask(_bootstrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
try {
|
await ref.read(appBootstrapProvider.notifier).bootstrap();
|
||||||
// Загружаем список домов
|
|
||||||
await ref.read(homesProvider.notifier).load();
|
|
||||||
await ref.read(currentHomeProvider.notifier).load();
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final home = ref.read(currentHomeProvider);
|
final bootstrap = ref.read(appBootstrapProvider);
|
||||||
|
switch (bootstrap.status) {
|
||||||
if (home != null) {
|
case AppBootstrapStatus.ready:
|
||||||
// Есть дом -- идём на пульт управления
|
|
||||||
await ref.read(groupsProvider.notifier).initAndRefresh();
|
|
||||||
// Загружаем info об авторизации (admin / не admin)
|
|
||||||
await ref.read(authInfoProvider.notifier).load();
|
|
||||||
|
|
||||||
// Запускаем / обновляем геофенс-таск если нужно
|
|
||||||
await syncGeofenceTask(ref.read(homesProvider));
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const RemoteScreen()),
|
MaterialPageRoute(builder: (_) => const RemoteScreen()),
|
||||||
);
|
);
|
||||||
}
|
break;
|
||||||
} else {
|
case AppBootstrapStatus.noHomes:
|
||||||
// Нет домов -- на экран управления домами
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const HomesScreen()),
|
MaterialPageRoute(builder: (_) => const HomesScreen()),
|
||||||
);
|
);
|
||||||
}
|
break;
|
||||||
}
|
case AppBootstrapStatus.bootstrapping:
|
||||||
} catch (e) {
|
case AppBootstrapStatus.authFailed:
|
||||||
debugPrint("Ошибка бутстрапа: $e");
|
case AppBootstrapStatus.networkFailed:
|
||||||
if (mounted) {
|
case AppBootstrapStatus.invalidConfig:
|
||||||
Navigator.of(context).pushReplacement(
|
case AppBootstrapStatus.failed:
|
||||||
MaterialPageRoute(builder: (_) => const HomesScreen()),
|
break;
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bootstrap = ref.watch(appBootstrapProvider);
|
||||||
|
final message = bootstrap.message;
|
||||||
|
|
||||||
|
if (!bootstrap.isBootstrapping && message != null) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: Colors.deepOrange,
|
||||||
|
size: 56,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Не удалось запустить приложение',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.white54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _bootstrap,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Повторить'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const HomesScreen()),
|
||||||
|
),
|
||||||
|
child: const Text('Открыть список домов'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import 'package:workmanager/workmanager.dart';
|
|||||||
/// Синглтон сервиса настроек
|
/// Синглтон сервиса настроек
|
||||||
final settingsServiceProvider = Provider((ref) => SettingsService());
|
final settingsServiceProvider = Provider((ref) => SettingsService());
|
||||||
|
|
||||||
/// API-клиент -- пересоздаётся при смене дома
|
/// API-клиент текущего дома. Конфигурация меняется через init().
|
||||||
final apiProvider = Provider((ref) => IgnisApi());
|
final apiProvider = Provider((ref) => IgnisApi());
|
||||||
|
|
||||||
// ─── Текущий дом ─────────────────────────────────────────────
|
// ─── Текущий дом ─────────────────────────────────────────────
|
||||||
@@ -43,8 +43,6 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
|
|||||||
await svc.setCurrentHomeId(home.id);
|
await svc.setCurrentHomeId(home.id);
|
||||||
state = home;
|
state = home;
|
||||||
await _initApi(home);
|
await _initApi(home);
|
||||||
// Перезагрузить группы для нового дома
|
|
||||||
await ref.read(groupsProvider.notifier).initAndRefresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Инициализировать API-клиент текущим домом
|
/// Инициализировать API-клиент текущим домом
|
||||||
@@ -234,9 +232,61 @@ final groupsProvider = NotifierProvider<GroupsNotifier, List<dynamic>>(
|
|||||||
() => GroupsNotifier(),
|
() => 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>> {
|
class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||||
IgnisApi get _api => ref.read(apiProvider);
|
IgnisApi get _api => ref.read(apiProvider);
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
bool _polling = false;
|
||||||
|
bool _refreshInFlight = false;
|
||||||
|
int? _refreshGeneration;
|
||||||
|
int _pollingGeneration = 0;
|
||||||
|
String? _pollingHomeId;
|
||||||
|
|
||||||
/// Блокировка обновления для группы после управления --
|
/// Блокировка обновления для группы после управления --
|
||||||
/// чтобы UI не прыгал пока лампа ещё не ответила.
|
/// чтобы UI не прыгал пока лампа ещё не ответила.
|
||||||
@@ -248,7 +298,7 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
|||||||
@override
|
@override
|
||||||
List<dynamic> build() {
|
List<dynamic> build() {
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
_timer?.cancel();
|
_stopPolling(resetStatus: false);
|
||||||
for (final t in _debounceTimers.values) {
|
for (final t in _debounceTimers.values) {
|
||||||
t.cancel();
|
t.cancel();
|
||||||
}
|
}
|
||||||
@@ -256,21 +306,58 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Инициализация: настроить API и начать периодический опрос
|
/// Настроить API и начать периодический опрос для текущего дома.
|
||||||
Future<void> initAndRefresh() async {
|
Future<void> startPolling() async {
|
||||||
final home = ref.read(currentHomeProvider);
|
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
|
final apiKey = await ref
|
||||||
.read(settingsServiceProvider)
|
.read(settingsServiceProvider)
|
||||||
.requireHomeApiKey(home.id);
|
.requireHomeApiKey(home.id);
|
||||||
_api.init(home.url, apiKey);
|
_api.init(home.url, apiKey);
|
||||||
|
|
||||||
await refresh();
|
await refresh();
|
||||||
_timer?.cancel();
|
if (!_isActiveGeneration(generation)) return;
|
||||||
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 10), (_) => refresh());
|
_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 {
|
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 {
|
try {
|
||||||
final resGroups = await _api.getGroups();
|
final resGroups = await _api.getGroups();
|
||||||
List<dynamic> rawList = [];
|
List<dynamic> rawList = [];
|
||||||
@@ -338,11 +425,25 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!_isActiveGeneration(generation)) return;
|
||||||
|
|
||||||
state = updatedList;
|
state = updatedList;
|
||||||
|
ref.read(groupsLoadStateProvider.notifier).setData(updatedList);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Ошибка глобального опроса: $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 не перетирал значения)
|
/// Установить блокировку на 5 секунд (чтобы UI не перетирал значения)
|
||||||
void _setLock(String id) =>
|
void _setLock(String id) =>
|
||||||
@@ -711,7 +812,7 @@ class AuthInfoNotifier extends Notifier<Map<String, dynamic>?> {
|
|||||||
@override
|
@override
|
||||||
Map<String, dynamic>? build() => null;
|
Map<String, dynamic>? build() => null;
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load({bool failOnError = false}) async {
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiProvider);
|
final api = ref.read(apiProvider);
|
||||||
final res = await api.getAuthMe();
|
final res = await api.getAuthMe();
|
||||||
@@ -720,6 +821,7 @@ class AuthInfoNotifier extends Notifier<Map<String, dynamic>?> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Ошибка загрузки auth/me: $e");
|
debugPrint("Ошибка загрузки auth/me: $e");
|
||||||
|
if (failOnError) rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,13 +180,21 @@ class _HomesScreenState extends ConsumerState<HomesScreen> {
|
|||||||
|
|
||||||
/// Выбрать дом и перейти на пульт
|
/// Выбрать дом и перейти на пульт
|
||||||
void _selectHome(BuildContext context, HomeConfig home) async {
|
void _selectHome(BuildContext context, HomeConfig home) async {
|
||||||
|
try {
|
||||||
await ref.read(currentHomeProvider.notifier).switchTo(home);
|
await ref.read(currentHomeProvider.notifier).switchTo(home);
|
||||||
await ref.read(authInfoProvider.notifier).load();
|
await ref.read(authInfoProvider.notifier).load(failOnError: true);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const RemoteScreen()),
|
MaterialPageRoute(builder: (_) => const RemoteScreen()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Не удалось выбрать дом: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавить новый дом
|
/// Добавить новый дом
|
||||||
|
|||||||
@@ -19,23 +19,22 @@ class RemoteScreen extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RemoteScreenState extends ConsumerState<RemoteScreen> {
|
class _RemoteScreenState extends ConsumerState<RemoteScreen> {
|
||||||
bool _loading = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_bootstrap();
|
Future.microtask(() => ref.read(groupsProvider.notifier).startPolling());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _bootstrap() async {
|
@override
|
||||||
await ref.read(groupsProvider.notifier).initAndRefresh();
|
void dispose() {
|
||||||
await ref.read(authInfoProvider.notifier).load();
|
ref.read(groupsProvider.notifier).stopPolling();
|
||||||
if (mounted) setState(() => _loading = false);
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final groups = ref.watch(groupsProvider);
|
final groups = ref.watch(groupsProvider);
|
||||||
|
final groupsLoadState = ref.watch(groupsLoadStateProvider);
|
||||||
final currentHome = ref.watch(currentHomeProvider);
|
final currentHome = ref.watch(currentHomeProvider);
|
||||||
final authInfo = ref.watch(authInfoProvider);
|
final authInfo = ref.watch(authInfoProvider);
|
||||||
final isAdmin = authInfo?['is_admin'] == true;
|
final isAdmin = authInfo?['is_admin'] == true;
|
||||||
@@ -124,7 +123,7 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _loading
|
body: groupsLoadState.isLoading && groups.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -138,6 +137,8 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
: groupsLoadState.hasError && groups.isEmpty
|
||||||
|
? _GroupsErrorView(message: groupsLoadState.errorMessage)
|
||||||
: groups.isEmpty
|
: groups.isEmpty
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -241,3 +242,48 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GroupsErrorView extends ConsumerWidget {
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
const _GroupsErrorView({this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.wifi_off_rounded,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.deepOrange,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Не удалось загрузить группы',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||||
|
),
|
||||||
|
if (message != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
message!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => ref.read(groupsProvider.notifier).refresh(),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Повторить'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user