diff --git a/lib/model/flood_station.dart b/lib/model/flood_station.dart index 09e88ea..b6be266 100644 --- a/lib/model/flood_station.dart +++ b/lib/model/flood_station.dart @@ -30,12 +30,15 @@ class FloodStation { riverName: parseStringValue(json['riverName']), ); + // sometimes the API returns a String instead of a double static double parseDoubleValue(dynamic value) { if (value is double) return value; if (value is String) return double.parse(value); return 0; } + // sometimes the API returns a list of labels that are basically identical + /// if [value] is a List, the method return the first item static String parseStringValue(dynamic value) { if (value is String) return value; if (value is List) return value[0]; diff --git a/lib/pages/landing_page.dart b/lib/pages/landing_page.dart index 43d3d28..9bc0205 100644 --- a/lib/pages/landing_page.dart +++ b/lib/pages/landing_page.dart @@ -17,7 +17,7 @@ class LandingPage extends StatefulWidget { // uses mixin OverlayService to show loading overlay class _LandingPageState extends State with OverlayService { - late FloodStationProvider floodStationProvider; + late FloodStationProvider _floodStationProvider; @override void dispose() { @@ -27,7 +27,7 @@ class _LandingPageState extends State with OverlayService { @override Widget build(BuildContext context) { - floodStationProvider = context.watch(); + _floodStationProvider = context.watch(); return Column( children: [ StationFilter( @@ -54,9 +54,9 @@ class _LandingPageState extends State with OverlayService { return Expanded( child: FloodStationListView( - stations: floodStationProvider.filtered - ? floodStationProvider.filteredStations - : floodStationProvider.allStations, + stations: _floodStationProvider.filtered + ? _floodStationProvider.filteredStations + : _floodStationProvider.allStations, onItemTapped: _navigateToStationDetail, ), ); @@ -64,7 +64,7 @@ class _LandingPageState extends State with OverlayService { void _handleFilterChange(String filterText) { if (filterText.isEmpty) { - floodStationProvider.filtered = false; + _floodStationProvider.filtered = false; setState(() {}); return; } @@ -73,14 +73,14 @@ class _LandingPageState extends State with OverlayService { context: context, message: 'Loading', onDismiss: () { - floodStationProvider.cancelFilterLoading(); + _floodStationProvider.cancelFilterLoading(); }, ); - floodStationProvider.loadFilteredStations(filterText); - floodStationProvider.filteredStationsFuture + _floodStationProvider.loadFilteredStations(filterText); + _floodStationProvider.filteredStationsFuture ?.then((_) => removeLoadingNotifier()); - floodStationProvider.filtered = true; + _floodStationProvider.filtered = true; } void _handleLoadAllStations() { @@ -88,27 +88,27 @@ class _LandingPageState extends State with OverlayService { context: context, message: 'Loading', onDismiss: () { - floodStationProvider.cancelFilterLoading(); + _floodStationProvider.cancelFilterLoading(); }, ); - floodStationProvider + _floodStationProvider .loadAllStations() .whenComplete(() => removeLoadingNotifier()); } void _navigateToStationDetail(FloodStation station) { - floodStationProvider.selectedStation = station; + _floodStationProvider.selectedStation = station; Navigator.of(context).pushNamed(FloodStationPage.routeName); } // returns boolean to decide whether the list of stations should be shown // if the list of stations is empty and is not filtered either, the function returns false bool get _shouldShowList { - if (!floodStationProvider.filtered && - floodStationProvider.allStations.isNotEmpty) { + if (!_floodStationProvider.filtered && + _floodStationProvider.allStations.isNotEmpty) { return true; } - return floodStationProvider.filtered; + return _floodStationProvider.filtered; } } diff --git a/lib/pages/main_navigation_scaffold.dart b/lib/pages/main_navigation_scaffold.dart index b14f9aa..b7a7bd5 100644 --- a/lib/pages/main_navigation_scaffold.dart +++ b/lib/pages/main_navigation_scaffold.dart @@ -5,6 +5,7 @@ import 'map_page.dart'; class MainNavigationScaffold extends StatefulWidget { const MainNavigationScaffold({super.key}); + static const routeName = '/'; @override @@ -12,13 +13,13 @@ class MainNavigationScaffold extends StatefulWidget { } class _MainNavigationScaffoldState extends State { - int _selectedPageIndex = 0; - final List _pages = [ const LandingPage(), const MapPage(), ]; + int _selectedPageIndex = 0; + @override Widget build(BuildContext context) { return Scaffold( @@ -32,6 +33,7 @@ class _MainNavigationScaffoldState extends State { NavigationDestination(icon: Icon(Icons.map_outlined), label: 'Map'), ], ), + // Used IndexedStack to save the page state while navigating body: IndexedStack( index: _selectedPageIndex, children: _pages, diff --git a/lib/pages/map_page.dart b/lib/pages/map_page.dart index 47339b4..1561c47 100644 --- a/lib/pages/map_page.dart +++ b/lib/pages/map_page.dart @@ -20,7 +20,7 @@ class MapPage extends StatefulWidget { } class _MapPageState extends State { - final mapController = MapController(); + final _mapController = MapController(); late FloodStationProvider _floodStationProvider; @override @@ -35,7 +35,7 @@ class _MapPageState extends State { ); } return FlutterMap( - mapController: mapController, + mapController: _mapController, options: MapOptions( cameraConstraint: CameraConstraint.containCenter( bounds: LatLngBounds.fromPoints(_floodStationProvider.allStations @@ -47,7 +47,7 @@ class _MapPageState extends State { initialZoom: 6, ), children: [ - openStreetMapTileLayer, + _openStreetMapTileLayer, MarkerClusterLayerWidget( options: MarkerClusterLayerOptions( maxClusterRadius: 45, @@ -56,13 +56,14 @@ class _MapPageState extends State { padding: EdgeInsets.all(50), maxZoom: 15, markers: _stationsAsMarkers(_floodStationProvider.allStations), - builder: _markerBuilder), + builder: _clusterMarkerBuilder), ) ], ); } - Widget _markerBuilder(BuildContext context, List markers) { + // builds the clustered marker + Widget _clusterMarkerBuilder(BuildContext context, List markers) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), @@ -71,12 +72,13 @@ class _MapPageState extends State { child: Center( child: Text( markers.length.toString(), - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onTertiary), ), ), ); } + // gets a list of markers from the list of all stations List _stationsAsMarkers(List stations) { return stations .map( @@ -91,7 +93,7 @@ class _MapPageState extends State { .toList(); } - _markerTapped(FloodStation station) { + void _markerTapped(FloodStation station) { showDialog( context: context, builder: (context) => MapPopup( @@ -104,7 +106,7 @@ class _MapPageState extends State { } } -TileLayer get openStreetMapTileLayer => TileLayer( +TileLayer get _openStreetMapTileLayer => TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', tileProvider: CancellableNetworkTileProvider(), diff --git a/lib/services/api.dart b/lib/services/api.dart index ad6d69f..98e53e1 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -9,6 +9,7 @@ class Api { static const String _rootUrl = 'https://environment.data.gov.uk/flood-monitoring'; + /// Fetches all stationszt static Future> fetchAllStations() async { List stations = []; final response = await http.get(Uri.parse('$_rootUrl/id/stations')); @@ -37,7 +38,6 @@ class Api { /// [limit] limits the number of entries that are requested from the API and [offset] returns the /// list starting from the specified number - static Future> fetchStationsByRange( int limit, int offset) async { List stations = []; @@ -52,6 +52,7 @@ class Api { return stations; } + /// Fetches all readings from the station with the specified [stationId] from the last 24h static Future> fetchReadingsFromStation( String stationId) async { List readings = []; diff --git a/lib/services/flood_station_provider.dart b/lib/services/flood_station_provider.dart index 9c63fa1..e8de0d2 100644 --- a/lib/services/flood_station_provider.dart +++ b/lib/services/flood_station_provider.dart @@ -4,19 +4,20 @@ import '../model/flood_station.dart'; import 'api.dart'; class FloodStationProvider extends ChangeNotifier { + // since the getter and setter for the following two fields would be empty, they are publicly accessible FloodStation? selectedStation; bool filtered = false; List _allStations = []; List _filteredStations = []; + CancelableOperation? _filteredStationsFuture; List get allStations => _allStations; List get filteredStations => _filteredStations; - - CancelableOperation? _filteredStationsFuture; - CancelableOperation? get filteredStationsFuture => _filteredStationsFuture; + /// loads all stations in batches of 500 and notifies listeners with every loop except if [silent] = true + /// this has lower performance than loading them all at once an shouldn't be used Future loadAllStationsInBatches({silent = false}) { int offset = 0; return Future.doWhile(() async { @@ -34,6 +35,7 @@ class FloodStationProvider extends ChangeNotifier { }); } + /// loads all flood stations and notifies listeners when done Future loadAllStations({silent = false}) { return Api.fetchAllStations().then( (value) { @@ -45,6 +47,7 @@ class FloodStationProvider extends ChangeNotifier { ); } + /// loads all stations whose label contains [filter] Future loadFilteredStations(String filter, {silent = false}) { if (_filteredStationsFuture != null) { _filteredStationsFuture!.cancel(); @@ -61,6 +64,7 @@ class FloodStationProvider extends ChangeNotifier { return future; } + /// cancels loading of filtered results. void cancelFilterLoading() { if (_filteredStationsFuture != null) { _filteredStationsFuture!.cancel(); diff --git a/lib/services/overlay_service.dart b/lib/services/overlay_service.dart index 492a0fc..4d61ee6 100644 --- a/lib/services/overlay_service.dart +++ b/lib/services/overlay_service.dart @@ -15,7 +15,7 @@ mixin OverlayService { final overlayState = Overlay.of(context); _overlayEntry = OverlayEntry( builder: (context) => Positioned( - bottom: 85, + bottom: 85, // Positioned above the NavigationBar left: 0, right: 0, child: Center( diff --git a/lib/widgets/custom_marker.dart b/lib/widgets/custom_marker.dart index 6ab80c8..60b4374 100644 --- a/lib/widgets/custom_marker.dart +++ b/lib/widgets/custom_marker.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; class CustomMarker extends StatelessWidget { - const CustomMarker({super.key, required this.onTap}); - final void Function() onTap; + const CustomMarker({super.key, required void Function() onTap}) + : _onTap = onTap; + + final void Function() _onTap; @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: _onTap, child: Icon( Icons.location_on_sharp, color: Theme.of(context).colorScheme.primary, diff --git a/lib/widgets/flood_station_list_view.dart b/lib/widgets/flood_station_list_view.dart index 24807d7..f2127b7 100644 --- a/lib/widgets/flood_station_list_view.dart +++ b/lib/widgets/flood_station_list_view.dart @@ -6,11 +6,12 @@ class FloodStationListView extends StatelessWidget { const FloodStationListView( {super.key, required List stations, - required this.onItemTapped}) - : _stations = stations; + required void Function(FloodStation) onItemTapped}) + : _onItemTapped = onItemTapped, + _stations = stations; final List _stations; - final void Function(FloodStation) onItemTapped; + final void Function(FloodStation) _onItemTapped; @override Widget build(BuildContext context) { @@ -23,7 +24,7 @@ class FloodStationListView extends StatelessWidget { final item = _stations.elementAt(index); return ListTile( isThreeLine: true, - onTap: () => onItemTapped(item), + onTap: () => _onItemTapped(item), title: Text(item.label), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/loading_notifier.dart b/lib/widgets/loading_notifier.dart index 29626fa..423bbc8 100644 --- a/lib/widgets/loading_notifier.dart +++ b/lib/widgets/loading_notifier.dart @@ -2,9 +2,14 @@ import 'package:flutter/material.dart'; class LoadingNotifier extends StatelessWidget { const LoadingNotifier( - {super.key, required this.onDismissed, required this.message}); - final void Function() onDismissed; - final String message; + {super.key, + required void Function() onDismissed, + required String message}) + : _onDismissed = onDismissed, + _message = message; + + final void Function() _onDismissed; + final String _message; @override Widget build(BuildContext context) { @@ -26,11 +31,11 @@ class LoadingNotifier extends StatelessWidget { child: CircularProgressIndicator(), ), Text( - message, + _message, style: Theme.of(context).textTheme.titleSmall, ), IconButton( - onPressed: () => onDismissed(), + onPressed: () => _onDismissed(), icon: Icon(Icons.close), ) ], diff --git a/lib/widgets/map_popup.dart b/lib/widgets/map_popup.dart index 57491a8..0088101 100644 --- a/lib/widgets/map_popup.dart +++ b/lib/widgets/map_popup.dart @@ -5,14 +5,19 @@ import '../model/flood_station.dart'; class MapPopup extends StatelessWidget { const MapPopup( - {super.key, required this.station, required this.onShowTapped}); - final FloodStation station; - final Function() onShowTapped; + {super.key, + required FloodStation station, + required dynamic Function() onShowTapped}) + : _onShowTapped = onShowTapped, + _station = station; + + final FloodStation _station; + final Function() _onShowTapped; @override Widget build(BuildContext context) { return AlertDialog( - title: Text(station.label), + title: Text(_station.label), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -22,14 +27,14 @@ class MapPopup extends StatelessWidget { children: [ Icon(Icons.home_outlined), Padding(padding: EdgeInsets.only(left: 8)), - Text(station.town.isEmpty ? '-' : station.town), + Text(_station.town.isEmpty ? '-' : _station.town), ], ), Row( children: [ Icon(Icons.water), Padding(padding: EdgeInsets.only(left: 8)), - Text(station.riverName.isEmpty ? '-' : station.riverName), + Text(_station.riverName.isEmpty ? '-' : _station.riverName), ], ), Row( @@ -37,8 +42,8 @@ class MapPopup extends StatelessWidget { Icon(Icons.calendar_month_outlined), Padding(padding: EdgeInsets.only(left: 8)), Text( - station.dateOpened != null - ? intl.DateFormat.yMd().format(station.dateOpened!) + _station.dateOpened != null + ? intl.DateFormat.yMd().format(_station.dateOpened!) : '-', ), ], @@ -51,7 +56,7 @@ class MapPopup extends StatelessWidget { child: Text('dismiss'), ), TextButton( - onPressed: onShowTapped, + onPressed: _onShowTapped, child: Text('show'), ), ], diff --git a/lib/widgets/station_filter.dart b/lib/widgets/station_filter.dart index d6fdc1a..41f41a8 100644 --- a/lib/widgets/station_filter.dart +++ b/lib/widgets/station_filter.dart @@ -9,28 +9,28 @@ class StationFilter extends StatefulWidget { } class StationFilterState extends State { - TextEditingController filterController = TextEditingController(); + final TextEditingController _filterController = TextEditingController(); @override Widget build(BuildContext context) { return TextField( - controller: filterController, + controller: _filterController, decoration: InputDecoration( prefixIcon: Icon(Icons.search), suffixIcon: Opacity( - opacity: filterController.text.isEmpty ? 0 : 1, + opacity: _filterController.text.isEmpty ? 0 : 1, child: IconButton( onPressed: () { - filterController.clear(); + _filterController.clear(); - widget.onChanged(filterController.text); + widget.onChanged(_filterController.text); }, icon: Icon(Icons.delete), ), ), label: Text('Filter'), ), - onChanged: (_) => widget.onChanged(filterController.text), + onChanged: (_) => widget.onChanged(_filterController.text), ); } }