diff --git a/place_tracker/analysis_options.yaml b/place_tracker/analysis_options.yaml new file mode 100644 index 000000000..1a52a34fc --- /dev/null +++ b/place_tracker/analysis_options.yaml @@ -0,0 +1,10 @@ +analyzer: + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: warning + # treat missing returns as a warning (not a hint) + missing_return: warning + +linter: + rules: + - unawaited_futures diff --git a/place_tracker/ios/Podfile b/place_tracker/ios/Podfile index 7c6cb6f63..b31234135 100644 --- a/place_tracker/ios/Podfile +++ b/place_tracker/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/place_tracker/ios/Runner/AppDelegate.m b/place_tracker/ios/Runner/AppDelegate.m index 59a72e90b..bd40e835e 100644 --- a/place_tracker/ios/Runner/AppDelegate.m +++ b/place_tracker/ios/Runner/AppDelegate.m @@ -1,10 +1,12 @@ #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" +#import "GoogleMaps/GoogleMaps.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GMSServices provideAPIKey:@"YOUR KEY HERE"]; [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; diff --git a/place_tracker/ios/Runner/Info.plist b/place_tracker/ios/Runner/Info.plist index 5c35fac65..6e4d55a8f 100644 --- a/place_tracker/ios/Runner/Info.plist +++ b/place_tracker/ios/Runner/Info.plist @@ -41,5 +41,7 @@ UIViewControllerBasedStatusBarAppearance + io.flutter.embedded_views_preview + diff --git a/place_tracker/lib/place_details.dart b/place_tracker/lib/place_details.dart index 7b092dea8..de7bf3edd 100644 --- a/place_tracker/lib/place_details.dart +++ b/place_tracker/lib/place_details.dart @@ -25,6 +25,7 @@ class PlaceDetails extends StatefulWidget { class PlaceDetailsState extends State { Place _place; GoogleMapController _mapController; + final Set _markers = {}; final TextEditingController _nameController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @@ -39,7 +40,12 @@ class PlaceDetailsState extends State { void _onMapCreated(GoogleMapController controller) { _mapController = controller; - _mapController.addMarker(MarkerOptions(position: _place.latLng)); + setState(() { + _markers.add(Marker( + markerId: MarkerId(_place.latLng.toString()), + position: _place.latLng, + )); + }); } Widget _detailsBody() { @@ -74,6 +80,7 @@ class PlaceDetailsState extends State { center: _place.latLng, mapController: _mapController, onMapCreated: _onMapCreated, + markers: _markers, ), const _Reviews(), ], @@ -210,6 +217,7 @@ class _Map extends StatelessWidget { @required this.center, @required this.mapController, @required this.onMapCreated, + @required this.markers, Key key, }) : assert(center != null), assert(onMapCreated != null), @@ -218,6 +226,7 @@ class _Map extends StatelessWidget { final LatLng center; final GoogleMapController mapController; final ArgumentCallback onMapCreated; + final Set markers; @override Widget build(BuildContext context) { @@ -229,16 +238,15 @@ class _Map extends StatelessWidget { height: 240.0, child: GoogleMap( onMapCreated: onMapCreated, - options: GoogleMapOptions( - cameraPosition: CameraPosition( - target: center, - zoom: 16.0, - ), - zoomGesturesEnabled: false, - rotateGesturesEnabled: false, - tiltGesturesEnabled: false, - scrollGesturesEnabled: false, + initialCameraPosition: CameraPosition( + target: center, + zoom: 16.0, ), + markers: markers, + zoomGesturesEnabled: false, + rotateGesturesEnabled: false, + tiltGesturesEnabled: false, + scrollGesturesEnabled: false, ), ), ); diff --git a/place_tracker/lib/place_map.dart b/place_tracker/lib/place_map.dart index 5c6db4c5a..12fcaf404 100644 --- a/place_tracker/lib/place_map.dart +++ b/place_tracker/lib/place_map.dart @@ -41,49 +41,55 @@ class PlaceMapState extends State { return places.where((Place place) => place.category == category).toList(); } - GoogleMapController mapController; + Completer mapController = Completer(); + + MapType _currentMapType = MapType.normal; + + LatLng _lastMapPosition; + Map _markedPlaces = Map(); + + final Set _markers = {}; + Marker _pendingMarker; + MapConfiguration _configuration; void onMapCreated(GoogleMapController controller) async { - mapController = controller; - mapController.onInfoWindowTapped.add(_onInfoWindowTapped); + mapController.complete(controller); + _lastMapPosition = widget.center; // Draw initial place markers on creation so that we have something // interesting to look at. - final Map places = await _markPlaces(); - _zoomToFitPlaces( + setState(() { + for (Place place in AppState.of(context).places) { + _markers.add(_createPlaceMarker(place)); + } + }); + + // Zoom to fit the initially selected category. + await _zoomToFitPlaces( _getPlacesForCategory( AppState.of(context).selectedCategory, - places.values.toList(), + _markedPlaces.values.toList(), ), ); } - Future> _markPlaces() async { - await Future.wait( - AppState.of(context).places.map((Place place) => _markPlace(place))); - return _markedPlaces; - } - - Future _markPlace(Place place) async { - final Marker marker = await mapController.addMarker( - MarkerOptions( - position: place.latLng, - icon: _getPlaceMarkerIcon(place.category), - infoWindowText: InfoWindowText( - place.name, - '${place.starRating} Star Rating', - ), - visible: place.category == AppState.of(context).selectedCategory, + Marker _createPlaceMarker(Place place) { + final marker = Marker( + markerId: MarkerId(place.latLng.toString()), + position: place.latLng, + infoWindow: InfoWindow( + title: place.name, + snippet: '${place.starRating} Star Rating', + onTap: () => _pushPlaceDetailsScreen(place), ), + icon: _getPlaceMarkerIcon(place.category), + visible: place.category == AppState.of(context).selectedCategory, ); _markedPlaces[marker] = place; - } - - void _onInfoWindowTapped(Marker marker) { - _pushPlaceDetailsScreen(_markedPlaces[marker]); + return marker; } void _pushPlaceDetailsScreen(Place place) { @@ -120,55 +126,64 @@ class PlaceMapState extends State { AppState.updateWith(context, places: newPlaces); } - Future _updateExistingPlaceMarker({@required Place place}) async { + void _updateExistingPlaceMarker({@required Place place}) { Marker marker = _markedPlaces.keys .singleWhere((Marker value) => _markedPlaces[value].id == place.id); - // Set marker visibility to false to ensure the info window is hidden. Once - // the plugin fully supports the Google Maps API, use hideInfoWindow() - // instead. - await mapController.updateMarker( - marker, - MarkerOptions( - visible: false, - ), - ); - await mapController.updateMarker( - marker, - MarkerOptions( - infoWindowText: InfoWindowText( - place.name, - place.starRating != 0 ? '${place.starRating} Star Rating' : null, + setState(() { + final updatedMarker = marker.copyWith( + infoWindowParam: InfoWindow( + title: place.name, + snippet: + place.starRating != 0 ? '${place.starRating} Star Rating' : null, ), - visible: true, - ), - ); + ); + _updateMarker(marker: marker, updatedMarker: updatedMarker, place: place); + }); + } - _markedPlaces[marker] = place; + void _updateMarker({ + @required Marker marker, + @required Marker updatedMarker, + @required Place place, + }) { + _markers.remove(marker); + _markedPlaces.remove(marker); + + _markers.add(updatedMarker); + _markedPlaces[updatedMarker] = place; } - void _switchSelectedCategory(PlaceCategory category) { + Future _switchSelectedCategory(PlaceCategory category) async { AppState.updateWith(context, selectedCategory: category); - _showPlacesForSelectedCategory(category); + await _showPlacesForSelectedCategory(category); } Future _showPlacesForSelectedCategory(PlaceCategory category) async { - await Future.wait( - _markedPlaces.keys.map( - (Marker marker) => mapController.updateMarker( - marker, - MarkerOptions( - visible: _markedPlaces[marker].category == category, - ), - ), - ), - ); + setState(() { + for (Marker marker in List.of(_markedPlaces.keys)) { + final place = _markedPlaces[marker]; + final updatedMarker = marker.copyWith( + visibleParam: place.category == category, + ); - _zoomToFitPlaces( - _getPlacesForCategory(category, _markedPlaces.values.toList())); + _updateMarker( + marker: marker, + updatedMarker: updatedMarker, + place: place, + ); + } + }); + + await _zoomToFitPlaces(_getPlacesForCategory( + category, + _markedPlaces.values.toList(), + )); } - void _zoomToFitPlaces(List places) { + Future _zoomToFitPlaces(List places) async { + GoogleMapController controller = await mapController.future; + // Default min/max values to latitude and longitude of center. double minLat = widget.center.latitude; double maxLat = widget.center.latitude; @@ -182,7 +197,7 @@ class PlaceMapState extends State { maxLong = max(maxLong, place.longitude); } - mapController.animateCamera( + await controller.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: LatLng(minLat, minLong), @@ -194,40 +209,49 @@ class PlaceMapState extends State { } void _onAddPlacePressed() async { - Marker newMarker = await mapController.addMarker( - MarkerOptions( - position: LatLng( - mapController.cameraPosition.target.latitude, - mapController.cameraPosition.target.longitude, - ), + setState(() { + final newMarker = Marker( + markerId: MarkerId(_lastMapPosition.toString()), + position: _lastMapPosition, + infoWindow: InfoWindow(title: 'New Place'), draggable: true, icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), - ), - ); - setState(() { + ); + _markers.add(newMarker); _pendingMarker = newMarker; }); } void _confirmAddPlace(BuildContext context) async { if (_pendingMarker != null) { - await mapController.updateMarker( - _pendingMarker, - MarkerOptions( - icon: _getPlaceMarkerIcon(AppState.of(context).selectedCategory), - infoWindowText: InfoWindowText('New Place', null), - draggable: false, - ), - ); // Create a new Place and map it to the marker we just added. final Place newPlace = Place( id: Uuid().v1(), - latLng: _pendingMarker.options.position, - name: _pendingMarker.options.infoWindowText.title, + latLng: _pendingMarker.position, + name: _pendingMarker.infoWindow.title, category: AppState.of(context).selectedCategory, ); - _markedPlaces[_pendingMarker] = newPlace; + + setState(() { + final updatedMarker = _pendingMarker.copyWith( + iconParam: _getPlaceMarkerIcon(AppState.of(context).selectedCategory), + infoWindowParam: InfoWindow( + title: 'New Place', + snippet: null, + onTap: () => _pushPlaceDetailsScreen(newPlace), + ), + draggableParam: false, + ); + + _updateMarker( + marker: _pendingMarker, + updatedMarker: updatedMarker, + place: newPlace, + ); + + _pendingMarker = null; + }); // Show a confirmation snackbar that has an action to edit the new place. Scaffold.of(context).showSnackBar( @@ -257,29 +281,25 @@ class PlaceMapState extends State { ); AppState.updateWith(context, places: newPlaces); - - setState(() { - _pendingMarker = null; - }); } } void _cancelAddPlace() { if (_pendingMarker != null) { - mapController.removeMarker(_pendingMarker); setState(() { + _markers.remove(_pendingMarker); _pendingMarker = null; }); } } void _onToggleMapTypePressed() { - final MapType nextType = MapType.values[ - (mapController.options.mapType.index + 1) % MapType.values.length]; + final MapType nextType = + MapType.values[(_currentMapType.index + 1) % MapType.values.length]; - mapController.updateMapOptions( - GoogleMapOptions(mapType: nextType), - ); + setState(() { + _currentMapType = nextType; + }); } Future _maybeUpdateMapConfiguration() async { @@ -297,16 +317,15 @@ class PlaceMapState extends State { newConfiguration.selectedCategory) { // If the configuration change is only a category change, just update // the marker visibilities. - _showPlacesForSelectedCategory(newConfiguration.selectedCategory); + await _showPlacesForSelectedCategory(newConfiguration.selectedCategory); } 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. - await Future.wait( - newConfiguration.places - .where((Place p) => !_configuration.places.contains(p)) - .map((Place value) => _updateExistingPlaceMarker(place: value)), - ); - _zoomToFitPlaces( + newConfiguration.places + .where((Place p) => !_configuration.places.contains(p)) + .map((Place value) => _updateExistingPlaceMarker(place: value)); + + await _zoomToFitPlaces( _getPlacesForCategory( newConfiguration.selectedCategory, newConfiguration.places, @@ -331,13 +350,13 @@ class PlaceMapState extends State { children: [ GoogleMap( onMapCreated: onMapCreated, - options: GoogleMapOptions( - trackCameraPosition: true, - cameraPosition: CameraPosition( - target: widget.center, - zoom: 11.0, - ), + initialCameraPosition: CameraPosition( + target: widget.center, + zoom: 11.0, ), + mapType: _currentMapType, + markers: _markers, + onCameraMove: (position) => _lastMapPosition = position.target, ), _CategoryButtonBar( selectedPlaceCategory: AppState.of(context).selectedCategory, diff --git a/place_tracker/pubspec.yaml b/place_tracker/pubspec.yaml index 4a9adff57..18977287f 100644 --- a/place_tracker/pubspec.yaml +++ b/place_tracker/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: cupertino_icons: ^0.1.2 - google_maps_flutter: ^0.0.3 + google_maps_flutter: ^0.4.0 uuid: 1.0.3