refactored code to encapsulate date formatting

This commit is contained in:
2025-01-29 13:39:49 +01:00
parent 2655779fac
commit 7b7e3e9fe0
6 changed files with 85 additions and 43 deletions

View File

@@ -16,6 +16,8 @@ class FloodStationPage extends StatefulWidget {
} }
class _FloodStationPageState extends State<FloodStationPage> { class _FloodStationPageState extends State<FloodStationPage> {
bool _tableVisible = false;
@override @override
void deactivate() { void deactivate() {
context.read<FloodStationProvider>().selectedStation = null; context.read<FloodStationProvider>().selectedStation = null;
@@ -29,6 +31,14 @@ class _FloodStationPageState extends State<FloodStationPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(station?.label ?? ''), title: Text(station?.label ?? ''),
actions: [
TextButton(
onPressed: () => setState(() {
_tableVisible = !_tableVisible;
}),
child: _tableVisible ? Text('Show Graph') : Text('Show Table'),
),
],
), ),
body: FutureBuilder<List<Reading>>( body: FutureBuilder<List<Reading>>(
future: Api.fetchReadingsFromStation(station?.id ?? ''), future: Api.fetchReadingsFromStation(station?.id ?? ''),
@@ -36,6 +46,12 @@ class _FloodStationPageState extends State<FloodStationPage> {
if (snapshot.hasData) { if (snapshot.hasData) {
if (snapshot.data!.isEmpty) { if (snapshot.data!.isEmpty) {
return Center(child: Text('No readings on record.')); return Center(child: Text('No readings on record.'));
} else if (_tableVisible) {
return SingleChildScrollView(
child: Table(
children: _getTableChildren(snapshot.data!),
),
);
} }
return Center( return Center(
child: Padding( child: Padding(
@@ -53,4 +69,17 @@ class _FloodStationPageState extends State<FloodStationPage> {
}), }),
); );
} }
List<TableRow> _getTableChildren(List<Reading> list) {
return list
.map<TableRow>(
(e) => TableRow(
children: [
Text(e.dateTime.toString()),
Text(e.value.toString()),
],
),
)
.toList();
}
} }

View File

@@ -20,36 +20,22 @@ class MapPage extends StatefulWidget {
} }
class _MapPageState extends State<MapPage> { class _MapPageState extends State<MapPage> {
final _mapController = MapController(); final mapController = MapController();
late FloodStationProvider _floodStationProvider; late FloodStationProvider _floodStationProvider;
bool _loading = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_floodStationProvider = context.watch<FloodStationProvider>(); _floodStationProvider = context.watch<FloodStationProvider>();
if (_loading == true) { if (_floodStationProvider.allStations.isEmpty) {
return Center(
child: CircularProgressIndicator(),
);
} else if (_floodStationProvider.allStations.isEmpty) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: _floodStationProvider.loadAllStations,
setState(() {
_loading = true;
});
_floodStationProvider
.loadAllStations()
.whenComplete(() => setState(() {
_loading = false;
}));
},
child: Text('Load Map'), child: Text('Load Map'),
), ),
); );
} }
return FlutterMap( return FlutterMap(
mapController: _mapController, mapController: mapController,
options: MapOptions( options: MapOptions(
cameraConstraint: CameraConstraint.containCenter( cameraConstraint: CameraConstraint.containCenter(
bounds: LatLngBounds.fromPoints(_floodStationProvider.allStations bounds: LatLngBounds.fromPoints(_floodStationProvider.allStations
@@ -70,14 +56,13 @@ class _MapPageState extends State<MapPage> {
padding: EdgeInsets.all(50), padding: EdgeInsets.all(50),
maxZoom: 15, maxZoom: 15,
markers: _stationsAsMarkers(_floodStationProvider.allStations), markers: _stationsAsMarkers(_floodStationProvider.allStations),
builder: _clusterMarkerBuilder), builder: _markerBuilder),
) )
], ],
); );
} }
// builds the clustered marker Widget _markerBuilder(BuildContext context, List<Marker> markers) {
Widget _clusterMarkerBuilder(BuildContext context, List<Marker> markers) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -86,7 +71,7 @@ class _MapPageState extends State<MapPage> {
child: Center( child: Center(
child: Text( child: Text(
markers.length.toString(), markers.length.toString(),
style: TextStyle(color: Theme.of(context).colorScheme.onTertiary), style: const TextStyle(color: Colors.white),
), ),
), ),
); );
@@ -107,7 +92,7 @@ class _MapPageState extends State<MapPage> {
.toList(); .toList();
} }
void _markerTapped(FloodStation station) { _markerTapped(FloodStation station) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => MapPopup( builder: (context) => MapPopup(

View File

@@ -1,11 +1,13 @@
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'package:timezone/timezone.dart' as tz;
import '../model/flood_station.dart'; import '../model/flood_station.dart';
import '../model/reading.dart'; import '../model/reading.dart';
import 'date_utility.dart';
class Api { class Api {
Api._();
static const String _rootUrl = static const String _rootUrl =
'https://environment.data.gov.uk/flood-monitoring'; 'https://environment.data.gov.uk/flood-monitoring';
@@ -56,7 +58,8 @@ class Api {
static Future<List<Reading>> fetchReadingsFromStation( static Future<List<Reading>> fetchReadingsFromStation(
String stationId) async { String stationId) async {
List<Reading> readings = []; List<Reading> readings = [];
final dateTime = _getCurrentUKTime().subtract(Duration(days: 1)).toUtc(); final dateTime =
DateUtility.currentUKTimeUtc.subtract(Duration(days: 1)).toUtc();
final url = final url =
'$_rootUrl/id/stations/$stationId/readings?since=${dateTime.toIso8601String()}&_sorted'; '$_rootUrl/id/stations/$stationId/readings?since=${dateTime.toIso8601String()}&_sorted';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
@@ -68,9 +71,4 @@ class Api {
} }
return readings.reversed.toList(); return readings.reversed.toList();
} }
static DateTime _getCurrentUKTime() {
final london = tz.getLocation('Europe/London');
return tz.TZDateTime.now(london);
}
} }

View File

@@ -0,0 +1,36 @@
import 'package:timezone/timezone.dart' as tz;
import 'package:intl/intl.dart' as intl;
class DateUtility {
static final intl.DateFormat _hmFormat = intl.DateFormat('Hm');
static final intl.DateFormat _ymdhmFormat = intl.DateFormat('yyyy-MM-dd H:m');
static final intl.DateFormat _ymdFormat = intl.DateFormat('yyyy-MM-dd');
// private default contructor so class can't be instanciated
DateUtility._();
static DateTime get currentUKTimeUtc {
final london = tz.getLocation('Europe/London');
return tz.TZDateTime.now(london).toUtc();
}
/// Formats a date in minutesSinceEpoch to a formatted String of HH:mm
static String formatMinutesToHm(double value) {
return _hmFormat.format(
DateTime.fromMillisecondsSinceEpoch((value * 1000 * 60).toInt()));
}
static String formatDateToHm(DateTime date) {
return _hmFormat.format(date);
}
/// Formats a date to yyyy-MM-dd H:m
static String formatDateToYmdhm(DateTime date) {
return _ymdhmFormat.format(date);
}
/// Formats a date to yyyy-MM-dd
static String formatDateToYmd(DateTime date) {
return _ymdFormat.format(date);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
import '../model/flood_station.dart'; import '../model/flood_station.dart';
import '../services/date_utility.dart';
class MapPopup extends StatelessWidget { class MapPopup extends StatelessWidget {
const MapPopup( const MapPopup(
@@ -43,7 +43,7 @@ class MapPopup extends StatelessWidget {
Padding(padding: EdgeInsets.only(left: 8)), Padding(padding: EdgeInsets.only(left: 8)),
Text( Text(
_station.dateOpened != null _station.dateOpened != null
? intl.DateFormat.yMd().format(_station.dateOpened!) ? DateUtility.formatDateToYmd(_station.dateOpened!)
: '-', : '-',
), ),
], ],

View File

@@ -1,9 +1,9 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
import 'dart:math'; import 'dart:math';
import '../model/reading.dart'; import '../model/reading.dart';
import '../services/date_utility.dart';
class ReadingGraph extends StatelessWidget { class ReadingGraph extends StatelessWidget {
const ReadingGraph({super.key, required List<Reading> readings}) const ReadingGraph({super.key, required List<Reading> readings})
@@ -73,7 +73,7 @@ class ReadingGraph extends StatelessWidget {
minIncluded: false, minIncluded: false,
getTitlesWidget: (value, meta) => SideTitleWidget( getTitlesWidget: (value, meta) => SideTitleWidget(
meta: meta, meta: meta,
child: Text(_getDate(value)), child: Text(DateUtility.formatMinutesToHm(value)),
), ),
), ),
); );
@@ -94,23 +94,17 @@ class ReadingGraph extends StatelessWidget {
); );
} }
String _getDate(double value) {
intl.DateFormat hmFormat = intl.DateFormat('Hm');
return hmFormat.format(
DateTime.fromMillisecondsSinceEpoch((value * 1000 * 60).toInt()));
}
String _getLongDate(double value) { String _getLongDate(double value) {
DateTime date = DateTime date =
DateTime.fromMillisecondsSinceEpoch((value * 1000 * 60).toInt()); DateTime.fromMillisecondsSinceEpoch((value * 1000 * 60).toInt());
int daysDifference = (DateTime.now().weekday - date.weekday + 7) % 7; int daysDifference = (DateTime.now().weekday - date.weekday + 7) % 7;
if (daysDifference == 0) { if (daysDifference == 0) {
return 'Today ${intl.DateFormat('Hm').format(date)}'; return 'Today ${DateUtility.formatDateToHm(date)}';
} else if (daysDifference == 1) { } else if (daysDifference == 1) {
return 'Yesterday ${intl.DateFormat('Hm').format(date)}'; return 'Yesterday ${DateUtility.formatDateToHm(date)}';
} }
return intl.DateFormat('yyyy-MM-dd H:m').format(date); return DateUtility.formatDateToYmdhm(date);
} }
} }