diff --git a/place_tracker/lib/place.dart b/place_tracker/lib/place.dart index b29c89d1b..f922c983d 100644 --- a/place_tracker/lib/place.dart +++ b/place_tracker/lib/place.dart @@ -42,6 +42,27 @@ class Place { starRating: starRating ?? this.starRating, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Place && + runtimeType == other.runtimeType && + id == other.id && + latLng == other.latLng && + name == other.name && + category == other.category && + description == other.description && + starRating == other.starRating; + + @override + int get hashCode => + id.hashCode ^ + latLng.hashCode ^ + name.hashCode ^ + category.hashCode ^ + description.hashCode ^ + starRating.hashCode; } enum PlaceCategory { diff --git a/place_tracker/lib/place_details.dart b/place_tracker/lib/place_details.dart index fa9e677f9..ac26e521d 100644 --- a/place_tracker/lib/place_details.dart +++ b/place_tracker/lib/place_details.dart @@ -4,17 +4,17 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:provider/provider.dart'; import 'place.dart'; +import 'place_tracker_app.dart'; import 'stub_data.dart'; class PlaceDetails extends StatefulWidget { final Place place; - final ValueChanged onChanged; const PlaceDetails({ required this.place, - required this.onChanged, super.key, }); @@ -41,7 +41,7 @@ class _PlaceDetailsState extends State { child: IconButton( icon: const Icon(Icons.save, size: 30.0), onPressed: () { - widget.onChanged(_place); + _onChanged(_place); Navigator.pop(context); }, ), @@ -61,7 +61,7 @@ class _PlaceDetailsState extends State { void initState() { _place = widget.place; _nameController.text = _place.name; - _descriptionController.text = _place.description!; + _descriptionController.text = _place.description ?? ''; return super.initState(); } @@ -113,12 +113,22 @@ class _PlaceDetailsState extends State { )); }); } + + void _onChanged(Place value) { + // Replace the place with the modified version. + final newPlaces = List.from(context.read().places); + final index = newPlaces.indexWhere((place) => place.id == value.id); + newPlaces[index] = value; + + context.read().setPlaces(newPlaces); + } } class _DescriptionTextField extends StatelessWidget { final TextEditingController controller; final ValueChanged onChanged; + const _DescriptionTextField({ required this.controller, required this.onChanged, @@ -151,6 +161,7 @@ class _Map extends StatelessWidget { final GoogleMapController? mapController; final ArgumentCallback onMapCreated; final Set markers; + const _Map({ required this.center, required this.mapController, @@ -187,6 +198,7 @@ class _NameTextField extends StatelessWidget { final TextEditingController controller; final ValueChanged onChanged; + const _NameTextField({ required this.controller, required this.onChanged, @@ -304,6 +316,7 @@ class _StarBar extends StatelessWidget { final int rating; final ValueChanged onChanged; + const _StarBar({ required this.rating, required this.onChanged, diff --git a/place_tracker/lib/place_list.dart b/place_tracker/lib/place_list.dart index 6f0a32e78..e87b5ca4c 100644 --- a/place_tracker/lib/place_list.dart +++ b/place_tracker/lib/place_list.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'place.dart'; -import 'place_details.dart'; import 'place_tracker_app.dart'; class PlaceList extends StatefulWidget { @@ -35,10 +35,7 @@ class _PlaceListState extends State { shrinkWrap: true, children: state.places .where((place) => place.category == state.selectedCategory) - .map((place) => _PlaceListTile( - place: place, - onPlaceChanged: (value) => _onPlaceChanged(value), - )) + .map((place) => _PlaceListTile(place: place)) .toList(), ), ), @@ -51,16 +48,6 @@ class _PlaceListState extends State { Provider.of(context, listen: false) .setSelectedCategory(newCategory); } - - void _onPlaceChanged(Place value) { - // Replace the place with the modified version. - final newPlaces = - List.from(Provider.of(context, listen: false).places); - final index = newPlaces.indexWhere((place) => place.id == value.id); - newPlaces[index] = value; - - Provider.of(context, listen: false).setPlaces(newPlaces); - } } class _CategoryButton extends StatelessWidget { @@ -68,6 +55,7 @@ class _CategoryButton extends StatelessWidget { final bool selected; final ValueChanged onCategoryChanged; + const _CategoryButton({ required this.category, required this.selected, @@ -118,6 +106,7 @@ class _ListCategoryButtonBar extends StatelessWidget { final PlaceCategory selectedCategory; final ValueChanged onCategoryChanged; + const _ListCategoryButtonBar({ required this.selectedCategory, required this.onCategoryChanged, @@ -151,24 +140,14 @@ class _ListCategoryButtonBar extends StatelessWidget { class _PlaceListTile extends StatelessWidget { final Place place; - final ValueChanged onPlaceChanged; const _PlaceListTile({ required this.place, - required this.onPlaceChanged, }); @override Widget build(BuildContext context) { return InkWell( - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return PlaceDetails( - place: place, - onChanged: (value) => onPlaceChanged(value), - ); - }), - ), + onTap: () => context.go('/place/${place.id}'), child: Container( padding: const EdgeInsets.only(top: 16.0), child: Column( diff --git a/place_tracker/lib/place_map.dart b/place_tracker/lib/place_map.dart index f1f8efc9c..2ec80b9df 100644 --- a/place_tracker/lib/place_map.dart +++ b/place_tracker/lib/place_map.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; import 'place.dart'; -import 'place_details.dart'; import 'place_tracker_app.dart'; class MapConfiguration { @@ -77,11 +78,22 @@ class _PlaceMapState extends State { MapConfiguration? _configuration; + @override + void initState() { + super.initState(); + context.read().addListener(_watchMapConfigurationChanges); + } + + @override + void dispose() { + context.read().removeListener(_watchMapConfigurationChanges); + super.dispose(); + } + @override Widget build(BuildContext context) { - _maybeUpdateMapConfiguration(); + _watchMapConfigurationChanges(); var state = Provider.of(context); - return Builder(builder: (context) { // We need this additional builder here so that we can pass its context to // _AddPlaceButtonBar's onSavePressed callback. This callback shows a @@ -189,7 +201,7 @@ class _PlaceMapState extends State { infoWindowParam: InfoWindow( title: 'New Place', snippet: null, - onTap: () => _pushPlaceDetailsScreen(newPlace), + onTap: () => context.go('/place/${newPlace.id}'), ), draggableParam: false, ); @@ -212,7 +224,7 @@ class _PlaceMapState extends State { action: SnackBarAction( label: 'Edit', onPressed: () async { - _pushPlaceDetailsScreen(newPlace); + context.go('/place/${newPlace.id}'); }, ), ), @@ -221,14 +233,6 @@ class _PlaceMapState extends State { // Add the new place to the places stored in appState. final newPlaces = List.from(appState.places)..add(newPlace); - // Manually update our map configuration here since our map is already - // updated with the new marker. Otherwise, the map would be reconfigured - // in the main build method due to a modified AppState. - _configuration = MapConfiguration( - places: newPlaces, - selectedCategory: appState.selectedCategory, - ); - appState.setPlaces(newPlaces); } } @@ -240,7 +244,7 @@ class _PlaceMapState extends State { infoWindow: InfoWindow( title: place.name, snippet: '${place.starRating} Star Rating', - onTap: () => _pushPlaceDetailsScreen(place), + onTap: () => context.go('/place/${place.id}'), ), icon: await _getPlaceMarkerIcon(context, place.category), visible: place.category == @@ -250,11 +254,10 @@ class _PlaceMapState extends State { return marker; } - Future _maybeUpdateMapConfiguration() async { - _configuration ??= - MapConfiguration.of(Provider.of(context, listen: false)); - final newConfiguration = - MapConfiguration.of(Provider.of(context, listen: false)); + Future _watchMapConfigurationChanges() async { + final appState = context.read(); + _configuration ??= MapConfiguration.of(appState); + final newConfiguration = MapConfiguration.of(appState); // Since we manually update [_configuration] when place or selectedCategory // changes come from the [place_map], we should only enter this if statement @@ -270,9 +273,14 @@ class _PlaceMapState extends State { } else { // At this point, we know the places have been updated from the list // view. We need to reconfigure the map to respect the updates. - newConfiguration.places - .where((p) => !_configuration!.places.contains(p)) - .map((value) => _updateExistingPlaceMarker(place: value)); + for (final place in newConfiguration.places) { + final oldPlace = + _configuration!.places.firstWhereOrNull((p) => p.id == place.id); + if (oldPlace == null || oldPlace != place) { + // New place or updated place. + _updateExistingPlaceMarker(place: place); + } + } await _zoomToFitPlaces( _getPlacesForCategory( @@ -299,27 +307,6 @@ class _PlaceMapState extends State { }); } - void _onPlaceChanged(Place value) { - // Replace the place with the modified version. - final newPlaces = - List.from(Provider.of(context, listen: false).places); - final index = newPlaces.indexWhere((place) => place.id == value.id); - newPlaces[index] = value; - - _updateExistingPlaceMarker(place: value); - - // Manually update our map configuration here since our map is already - // updated with the new marker. Otherwise, the map would be reconfigured - // in the main build method due to a modified AppState. - _configuration = MapConfiguration( - places: newPlaces, - selectedCategory: - Provider.of(context, listen: false).selectedCategory, - ); - - Provider.of(context, listen: false).setPlaces(newPlaces); - } - void _onToggleMapTypePressed() { final nextType = MapType.values[(_currentMapType.index + 1) % MapType.values.length]; @@ -329,18 +316,6 @@ class _PlaceMapState extends State { }); } - void _pushPlaceDetailsScreen(Place place) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return PlaceDetails( - place: place, - onChanged: (value) => _onPlaceChanged(value), - ); - }), - ); - } - Future _showPlacesForSelectedCategory(PlaceCategory category) async { setState(() { for (var marker in List.of(_markedPlaces.keys)) { @@ -412,15 +387,17 @@ class _PlaceMapState extends State { maxLong = max(maxLong, place.longitude); } - await controller.animateCamera( - CameraUpdate.newLatLngBounds( - LatLngBounds( - southwest: LatLng(minLat, minLong), - northeast: LatLng(maxLat, maxLong), + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.animateCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: LatLng(minLat, minLong), + northeast: LatLng(maxLat, maxLong), + ), + 48.0, ), - 48.0, - ), - ); + ); + }); } static Future _getPlaceMarkerIcon( diff --git a/place_tracker/lib/place_tracker_app.dart b/place_tracker/lib/place_tracker_app.dart index 1a9a41dea..7b0582dfa 100644 --- a/place_tracker/lib/place_tracker_app.dart +++ b/place_tracker/lib/place_tracker_app.dart @@ -3,10 +3,12 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; import 'place.dart'; +import 'place_details.dart'; import 'place_list.dart'; import 'place_map.dart'; import 'stub_data.dart'; @@ -21,8 +23,26 @@ class PlaceTrackerApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: _PlaceTrackerHomePage(), + return MaterialApp.router( + routerConfig: GoRouter(routes: [ + GoRoute( + path: '/', + builder: (context, state) => const _PlaceTrackerHomePage(), + routes: [ + GoRoute( + path: 'place/:id', + builder: (context, state) { + final id = state.params['id']!; + final place = context + .read() + .places + .singleWhere((place) => place.id == id); + return PlaceDetails(place: place); + }, + ), + ], + ), + ]), ); } } diff --git a/place_tracker/pubspec.yaml b/place_tracker/pubspec.yaml index b61f6c60c..ca5c02389 100644 --- a/place_tracker/pubspec.yaml +++ b/place_tracker/pubspec.yaml @@ -15,6 +15,8 @@ dependencies: google_maps_flutter_web: ">=0.3.0+1 <0.5.0" provider: ^6.0.2 uuid: ^3.0.4 + go_router: ^5.2.4 + collection: ^1.16.0 dev_dependencies: flutter_test: