Introduced auto refresh for API Data
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:ac_web/Api/Dtos/AgentDto.dart';
|
||||
import 'package:ac_web/Pages/AgentDetails.dart';
|
||||
import 'package:ac_web/Widgets/AgentStatusIndicator.dart';
|
||||
import 'package:ac_web/Widgets/AgentTypeIcons.dart';
|
||||
import 'package:ac_web/Widgets/AutoRefreshState.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Fetches all agents from the AgentsController and lists them with a colored
|
||||
@@ -14,7 +15,8 @@ class AgentAvailabilityColumn extends StatefulWidget{
|
||||
State<AgentAvailabilityColumn> createState() => _AgentAvailabilityColumnState();
|
||||
}
|
||||
|
||||
class _AgentAvailabilityColumnState extends State<AgentAvailabilityColumn>{
|
||||
class _AgentAvailabilityColumnState extends State<AgentAvailabilityColumn>
|
||||
with AutoRefreshState<AgentAvailabilityColumn> {
|
||||
|
||||
late Future<List<AgentDto>> mAgentsFuture;
|
||||
|
||||
@@ -24,23 +26,44 @@ class _AgentAvailabilityColumnState extends State<AgentAvailabilityColumn>{
|
||||
mAgentsFuture = AcApi.instance.agents.getAgents();
|
||||
}
|
||||
|
||||
/// Reload triggered by the user. Shows the loading indicator again because
|
||||
/// the user asked for the request and should see that it is running.
|
||||
void ReloadAgents(){
|
||||
setState(() {
|
||||
mAgentsFuture = AcApi.instance.agents.getAgents();
|
||||
});
|
||||
}
|
||||
|
||||
/// Automatic reload. The agents that are already on screen stay visible
|
||||
/// while the request runs, the future is only replaced once it succeeded.
|
||||
@override
|
||||
Future<void> RefreshData() async {
|
||||
|
||||
final List<AgentDto> agents = await AcApi.instance.agents.getAgents();
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
mAgentsFuture = Future<List<AgentDto>>.value(agents);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<AgentDto>>(
|
||||
future: mAgentsFuture,
|
||||
builder: (context, snapshot) {
|
||||
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
// Not `connectionState`: a refresh puts the builder back into the
|
||||
// waiting state although the previous agents are still available, and
|
||||
// those should stay on screen instead of flashing a spinner.
|
||||
if (!snapshot.hasData && !snapshot.hasError) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.hasError && !snapshot.hasData) {
|
||||
return Center(child: Text('Could not load agents: ${snapshot.error}'));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Keeps the data of a widget up to date by reloading it every
|
||||
/// [refreshInterval].
|
||||
///
|
||||
/// Mix this into the [State] of every widget that shows data of the backend and
|
||||
/// implement [RefreshData]. The mixin only owns the timer, fetching the data and
|
||||
/// putting it on screen stays in the widget itself.
|
||||
///
|
||||
/// The backend has no push channel, so the only way to notice new agents,
|
||||
/// tasks or reports is polling. The interval is therefore a compromise between
|
||||
/// fresh data and load on the api.
|
||||
///
|
||||
/// A refresh happens quietly: implementations are expected to keep the data
|
||||
/// that is already on screen while the request runs, so that the user neither
|
||||
/// sees a spinner nor loses the scroll position every half minute. Failures of
|
||||
/// a refresh are swallowed on purpose - the data on screen simply stays as it
|
||||
/// is and the next tick tries again.
|
||||
mixin AutoRefreshState<TWidget extends StatefulWidget> on State<TWidget> {
|
||||
|
||||
/// Interval in which the api data of the whole app is refreshed.
|
||||
static const Duration refreshInterval = Duration(seconds: 30);
|
||||
|
||||
Timer? _refreshTimer;
|
||||
|
||||
/// True while a refresh triggered by the timer is running. A slow api must
|
||||
/// not pile up requests.
|
||||
bool mIsAutoRefreshing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshTimer = Timer.periodic(refreshInterval, (_) => _onTick());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Fetches the data of this widget again. Called every [refreshInterval] as
|
||||
/// long as the widget is mounted.
|
||||
///
|
||||
/// Implementations may let api errors escape, [_onTick] catches them.
|
||||
Future<void> RefreshData();
|
||||
|
||||
Future<void> _onTick() async {
|
||||
|
||||
if (mIsAutoRefreshing || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
mIsAutoRefreshing = true;
|
||||
|
||||
try {
|
||||
await RefreshData();
|
||||
} catch (error) {
|
||||
// An automatic refresh must never replace data that is already on screen
|
||||
// with an error message, so a failed tick is ignored.
|
||||
} finally {
|
||||
mIsAutoRefreshing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:ac_web/Api/AcApi.dart';
|
||||
import 'package:ac_web/Api/Dtos/EReportType.dart';
|
||||
import 'package:ac_web/Api/Dtos/ReportDto.dart';
|
||||
import 'package:ac_web/Pages/ReportDetails.dart';
|
||||
import 'package:ac_web/Widgets/AutoRefreshState.dart';
|
||||
import 'package:ac_web/Widgets/LatestReportEntry.dart';
|
||||
import 'package:ac_web/Widgets/LatestReportHeader.dart';
|
||||
import 'package:ac_web/Widgets/ReportTypeIcons.dart';
|
||||
@@ -20,7 +21,8 @@ class LatestReportColumn extends StatefulWidget{
|
||||
State<LatestReportColumn> createState() => _LatestReportColumnState();
|
||||
}
|
||||
|
||||
class _LatestReportColumnState extends State<LatestReportColumn>{
|
||||
class _LatestReportColumnState extends State<LatestReportColumn>
|
||||
with AutoRefreshState<LatestReportColumn> {
|
||||
|
||||
late Future<List<ReportDto>> mReportsFuture;
|
||||
|
||||
@@ -30,17 +32,36 @@ class _LatestReportColumnState extends State<LatestReportColumn>{
|
||||
mReportsFuture = AcApi.instance.reports.getReports();
|
||||
}
|
||||
|
||||
/// Automatic reload. The reports that are already on screen stay visible
|
||||
/// while the request runs, the future is only replaced once it succeeded.
|
||||
@override
|
||||
Future<void> RefreshData() async {
|
||||
|
||||
final List<ReportDto> reports = await AcApi.instance.reports.getReports();
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
mReportsFuture = Future<List<ReportDto>>.value(reports);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<ReportDto>>(
|
||||
future: mReportsFuture,
|
||||
builder: (context, snapshot) {
|
||||
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
// Not `connectionState`: a refresh puts the builder back into the
|
||||
// waiting state although the previous reports are still available, and
|
||||
// those should stay on screen instead of flashing a spinner.
|
||||
if (!snapshot.hasData && !snapshot.hasError) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.hasError && !snapshot.hasData) {
|
||||
return Center(child: Text('Could not load reports: ${snapshot.error}'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user