1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 13:58:47 +00:00

Place tracker/maintenance (#519)

* format place_tracker README

* update sentence in README

* sort Dart members, remove unnecessary type declarations

* Run place_tracker on Android and iOS, update project files

* add link to developer preview caveat

* grammar

* update MAINTENANCE
This commit is contained in:
John Ryan
2020-08-13 14:30:28 -07:00
committed by GitHub
parent 00d0cdf02c
commit 437d1f620b
12 changed files with 765 additions and 734 deletions

View File

@@ -11,41 +11,55 @@ import 'place.dart';
import 'place_details.dart';
import 'place_tracker_app.dart';
class MapConfiguration {
final List<Place> places;
final PlaceCategory selectedCategory;
const MapConfiguration({
@required this.places,
@required this.selectedCategory,
}) : assert(places != null),
assert(selectedCategory != null);
@override
int get hashCode => places.hashCode ^ selectedCategory.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MapConfiguration &&
other.places == places &&
other.selectedCategory == selectedCategory;
}
static MapConfiguration of(AppState appState) {
return MapConfiguration(
places: appState.places,
selectedCategory: appState.selectedCategory,
);
}
}
class PlaceMap extends StatefulWidget {
final LatLng center;
const PlaceMap({
Key key,
this.center,
}) : super(key: key);
final LatLng center;
@override
PlaceMapState createState() => PlaceMapState();
}
class PlaceMapState extends State<PlaceMap> {
static Future<BitmapDescriptor> _getPlaceMarkerIcon(
BuildContext context, PlaceCategory category) async {
switch (category) {
case PlaceCategory.favorite:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/heart.png');
break;
case PlaceCategory.visited:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/visited.png');
break;
case PlaceCategory.wantToGo:
default:
return BitmapDescriptor.defaultMarker;
}
}
static List<Place> _getPlacesForCategory(
PlaceCategory category, List<Place> places) {
return places.where((place) => place.category == category).toList();
}
Completer<GoogleMapController> mapController = Completer();
MapType _currentMapType = MapType.normal;
@@ -60,6 +74,50 @@ class PlaceMapState extends State<PlaceMap> {
MapConfiguration _configuration;
@override
Widget build(BuildContext context) {
_maybeUpdateMapConfiguration();
var state = Provider.of<AppState>(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
// SnackBar and to do this, we need a build context that has Scaffold as
// an ancestor.
return Center(
child: Stack(
children: [
GoogleMap(
onMapCreated: onMapCreated,
initialCameraPosition: CameraPosition(
target: widget.center,
zoom: 11.0,
),
mapType: _currentMapType,
markers: _markers,
onCameraMove: (position) => _lastMapPosition = position.target,
),
_CategoryButtonBar(
selectedPlaceCategory: state.selectedCategory,
visible: _pendingMarker == null,
onChanged: _switchSelectedCategory,
),
_AddPlaceButtonBar(
visible: _pendingMarker != null,
onSavePressed: () => _confirmAddPlace(context),
onCancelPressed: _cancelAddPlace,
),
_MapFabs(
visible: _pendingMarker == null,
onAddPlacePressed: _onAddPlacePressed,
onToggleMapTypePressed: _onToggleMapTypePressed,
),
],
),
);
});
}
Future<void> onMapCreated(GoogleMapController controller) async {
mapController.complete(controller);
_lastMapPosition = widget.center;
@@ -83,152 +141,13 @@ class PlaceMapState extends State<PlaceMap> {
);
}
Future<Marker> _createPlaceMarker(BuildContext context, Place place) async {
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: await _getPlaceMarkerIcon(context, place.category),
visible: place.category ==
Provider.of<AppState>(context, listen: false).selectedCategory,
);
_markedPlaces[marker] = place;
return marker;
}
void _pushPlaceDetailsScreen(Place place) {
assert(place != null);
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (value) => _onPlaceChanged(value),
);
}),
);
}
void _onPlaceChanged(Place value) {
// Replace the place with the modified version.
final newPlaces =
List<Place>.from(Provider.of<AppState>(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<AppState>(context, listen: false).selectedCategory,
);
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
void _updateExistingPlaceMarker({@required Place place}) {
var marker = _markedPlaces.keys
.singleWhere((value) => _markedPlaces[value].id == place.id);
setState(() {
final updatedMarker = marker.copyWith(
infoWindowParam: InfoWindow(
title: place.name,
snippet:
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
),
);
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
});
}
void _updateMarker({
@required Marker marker,
@required Marker updatedMarker,
@required Place place,
}) {
_markers.remove(marker);
_markedPlaces.remove(marker);
_markers.add(updatedMarker);
_markedPlaces[updatedMarker] = place;
}
Future<void> _switchSelectedCategory(PlaceCategory category) async {
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
await _showPlacesForSelectedCategory(category);
}
Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async {
setState(() {
for (var marker in List.of(_markedPlaces.keys)) {
final place = _markedPlaces[marker];
final updatedMarker = marker.copyWith(
visibleParam: place.category == category,
);
_updateMarker(
marker: marker,
updatedMarker: updatedMarker,
place: place,
);
}
});
await _zoomToFitPlaces(_getPlacesForCategory(
category,
_markedPlaces.values.toList(),
));
}
Future<void> _zoomToFitPlaces(List<Place> places) async {
var controller = await mapController.future;
// Default min/max values to latitude and longitude of center.
var minLat = widget.center.latitude;
var maxLat = widget.center.latitude;
var minLong = widget.center.longitude;
var maxLong = widget.center.longitude;
for (var place in places) {
minLat = min(minLat, place.latitude);
maxLat = max(maxLat, place.latitude);
minLong = min(minLong, place.longitude);
maxLong = max(maxLong, place.longitude);
void _cancelAddPlace() {
if (_pendingMarker != null) {
setState(() {
_markers.remove(_pendingMarker);
_pendingMarker = null;
});
}
await controller.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLong),
northeast: LatLng(maxLat, maxLong),
),
48.0,
),
);
}
Future<void> _onAddPlacePressed() async {
setState(() {
final newMarker = Marker(
markerId: MarkerId(_lastMapPosition.toString()),
position: _lastMapPosition,
infoWindow: InfoWindow(title: 'New Place'),
draggable: true,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
);
_markers.add(newMarker);
_pendingMarker = newMarker;
});
}
Future<void> _confirmAddPlace(BuildContext context) async {
@@ -298,22 +217,21 @@ class PlaceMapState extends State<PlaceMap> {
}
}
void _cancelAddPlace() {
if (_pendingMarker != null) {
setState(() {
_markers.remove(_pendingMarker);
_pendingMarker = null;
});
}
}
void _onToggleMapTypePressed() {
final nextType =
MapType.values[(_currentMapType.index + 1) % MapType.values.length];
setState(() {
_currentMapType = nextType;
});
Future<Marker> _createPlaceMarker(BuildContext context, Place place) async {
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: await _getPlaceMarkerIcon(context, place.category),
visible: place.category ==
Provider.of<AppState>(context, listen: false).selectedCategory,
);
_markedPlaces[marker] = place;
return marker;
}
Future<void> _maybeUpdateMapConfiguration() async {
@@ -351,65 +269,183 @@ class PlaceMapState extends State<PlaceMap> {
}
}
@override
Widget build(BuildContext context) {
_maybeUpdateMapConfiguration();
var state = Provider.of<AppState>(context);
Future<void> _onAddPlacePressed() async {
setState(() {
final newMarker = Marker(
markerId: MarkerId(_lastMapPosition.toString()),
position: _lastMapPosition,
infoWindow: InfoWindow(title: 'New Place'),
draggable: true,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
);
_markers.add(newMarker);
_pendingMarker = newMarker;
});
}
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
// SnackBar and to do this, we need a build context that has Scaffold as
// an ancestor.
return Center(
child: Stack(
children: <Widget>[
GoogleMap(
onMapCreated: onMapCreated,
initialCameraPosition: CameraPosition(
target: widget.center,
zoom: 11.0,
),
mapType: _currentMapType,
markers: _markers,
onCameraMove: (position) => _lastMapPosition = position.target,
),
_CategoryButtonBar(
selectedPlaceCategory: state.selectedCategory,
visible: _pendingMarker == null,
onChanged: _switchSelectedCategory,
),
_AddPlaceButtonBar(
visible: _pendingMarker != null,
onSavePressed: () => _confirmAddPlace(context),
onCancelPressed: _cancelAddPlace,
),
_MapFabs(
visible: _pendingMarker == null,
onAddPlacePressed: _onAddPlacePressed,
onToggleMapTypePressed: _onToggleMapTypePressed,
),
],
void _onPlaceChanged(Place value) {
// Replace the place with the modified version.
final newPlaces =
List<Place>.from(Provider.of<AppState>(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<AppState>(context, listen: false).selectedCategory,
);
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
void _onToggleMapTypePressed() {
final nextType =
MapType.values[(_currentMapType.index + 1) % MapType.values.length];
setState(() {
_currentMapType = nextType;
});
}
void _pushPlaceDetailsScreen(Place place) {
assert(place != null);
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (value) => _onPlaceChanged(value),
);
}),
);
}
Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async {
setState(() {
for (var marker in List.of(_markedPlaces.keys)) {
final place = _markedPlaces[marker];
final updatedMarker = marker.copyWith(
visibleParam: place.category == category,
);
_updateMarker(
marker: marker,
updatedMarker: updatedMarker,
place: place,
);
}
});
await _zoomToFitPlaces(_getPlacesForCategory(
category,
_markedPlaces.values.toList(),
));
}
Future<void> _switchSelectedCategory(PlaceCategory category) async {
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
await _showPlacesForSelectedCategory(category);
}
void _updateExistingPlaceMarker({@required Place place}) {
var marker = _markedPlaces.keys
.singleWhere((value) => _markedPlaces[value].id == place.id);
setState(() {
final updatedMarker = marker.copyWith(
infoWindowParam: InfoWindow(
title: place.name,
snippet:
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
),
);
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
});
}
void _updateMarker({
@required Marker marker,
@required Marker updatedMarker,
@required Place place,
}) {
_markers.remove(marker);
_markedPlaces.remove(marker);
_markers.add(updatedMarker);
_markedPlaces[updatedMarker] = place;
}
Future<void> _zoomToFitPlaces(List<Place> places) async {
var controller = await mapController.future;
// Default min/max values to latitude and longitude of center.
var minLat = widget.center.latitude;
var maxLat = widget.center.latitude;
var minLong = widget.center.longitude;
var maxLong = widget.center.longitude;
for (var place in places) {
minLat = min(minLat, place.latitude);
maxLat = max(maxLat, place.latitude);
minLong = min(minLong, place.longitude);
maxLong = max(maxLong, place.longitude);
}
await controller.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLong),
northeast: LatLng(maxLat, maxLong),
),
48.0,
),
);
}
static Future<BitmapDescriptor> _getPlaceMarkerIcon(
BuildContext context, PlaceCategory category) async {
switch (category) {
case PlaceCategory.favorite:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/heart.png');
break;
case PlaceCategory.visited:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/visited.png');
break;
case PlaceCategory.wantToGo:
default:
return BitmapDescriptor.defaultMarker;
}
}
static List<Place> _getPlacesForCategory(
PlaceCategory category, List<Place> places) {
return places.where((place) => place.category == category).toList();
}
}
class _CategoryButtonBar extends StatelessWidget {
const _CategoryButtonBar({
Key key,
@required this.selectedPlaceCategory,
@required this.visible,
@required this.onChanged,
}) : assert(selectedPlaceCategory != null),
assert(visible != null),
assert(onChanged != null),
super(key: key);
final PlaceCategory selectedPlaceCategory;
class _AddPlaceButtonBar extends StatelessWidget {
final bool visible;
final ValueChanged<PlaceCategory> onChanged;
final VoidCallback onSavePressed;
final VoidCallback onCancelPressed;
const _AddPlaceButtonBar({
Key key,
@required this.visible,
@required this.onSavePressed,
@required this.onCancelPressed,
}) : assert(visible != null),
assert(onSavePressed != null),
assert(onCancelPressed != null),
super(key: key);
@override
Widget build(BuildContext context) {
@@ -420,7 +456,55 @@ class _CategoryButtonBar extends StatelessWidget {
alignment: Alignment.bottomCenter,
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
children: [
RaisedButton(
color: Colors.blue,
child: const Text(
'Save',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
onPressed: onSavePressed,
),
RaisedButton(
color: Colors.red,
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
onPressed: onCancelPressed,
),
],
),
),
);
}
}
class _CategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedPlaceCategory;
final bool visible;
final ValueChanged<PlaceCategory> onChanged;
const _CategoryButtonBar({
Key key,
@required this.selectedPlaceCategory,
@required this.visible,
@required this.onChanged,
}) : assert(selectedPlaceCategory != null),
assert(visible != null),
assert(onChanged != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Visibility(
visible: visible,
child: Container(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
alignment: Alignment.bottomCenter,
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: [
RaisedButton(
color: selectedPlaceCategory == PlaceCategory.favorite
? Colors.green[700]
@@ -458,55 +542,11 @@ class _CategoryButtonBar extends StatelessWidget {
}
}
class _AddPlaceButtonBar extends StatelessWidget {
const _AddPlaceButtonBar({
Key key,
@required this.visible,
@required this.onSavePressed,
@required this.onCancelPressed,
}) : assert(visible != null),
assert(onSavePressed != null),
assert(onCancelPressed != null),
super(key: key);
final bool visible;
final VoidCallback onSavePressed;
final VoidCallback onCancelPressed;
@override
Widget build(BuildContext context) {
return Visibility(
visible: visible,
child: Container(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
alignment: Alignment.bottomCenter,
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Colors.blue,
child: const Text(
'Save',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
onPressed: onSavePressed,
),
RaisedButton(
color: Colors.red,
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
onPressed: onCancelPressed,
),
],
),
),
);
}
}
class _MapFabs extends StatelessWidget {
final bool visible;
final VoidCallback onAddPlacePressed;
final VoidCallback onToggleMapTypePressed;
const _MapFabs({
Key key,
@required this.visible,
@@ -517,10 +557,6 @@ class _MapFabs extends StatelessWidget {
assert(onToggleMapTypePressed != null),
super(key: key);
final bool visible;
final VoidCallback onAddPlacePressed;
final VoidCallback onToggleMapTypePressed;
@override
Widget build(BuildContext context) {
return Container(
@@ -529,7 +565,7 @@ class _MapFabs extends StatelessWidget {
child: Visibility(
visible: visible,
child: Column(
children: <Widget>[
children: [
FloatingActionButton(
heroTag: 'add_place_button',
onPressed: onAddPlacePressed,
@@ -552,39 +588,3 @@ class _MapFabs extends StatelessWidget {
);
}
}
class MapConfiguration {
const MapConfiguration({
@required this.places,
@required this.selectedCategory,
}) : assert(places != null),
assert(selectedCategory != null);
final List<Place> places;
final PlaceCategory selectedCategory;
@override
int get hashCode => places.hashCode ^ selectedCategory.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MapConfiguration &&
other.places == places &&
other.selectedCategory == selectedCategory;
}
static MapConfiguration of(AppState appState) {
return MapConfiguration(
places: appState.places,
selectedCategory: appState.selectedCategory,
);
}
}