@ -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,6 +141,82 @@ class PlaceMapState extends State<PlaceMap> {
) ;
}
void _cancelAddPlace ( ) {
if ( _pendingMarker ! = null ) {
setState ( ( ) {
_markers . remove ( _pendingMarker ) ;
_pendingMarker = null ;
} ) ;
}
}
Future < void > _confirmAddPlace ( BuildContext context ) async {
if ( _pendingMarker ! = null ) {
/ / Create a new Place and map it to the marker we just added .
final newPlace = Place (
id: Uuid ( ) . v1 ( ) ,
latLng: _pendingMarker . position ,
name: _pendingMarker . infoWindow . title ,
category:
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
var placeMarker = await _getPlaceMarkerIcon ( context ,
Provider . of < AppState > ( context , listen: false ) . selectedCategory ) ;
setState ( ( ) {
final updatedMarker = _pendingMarker . copyWith (
iconParam: placeMarker ,
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 (
SnackBar (
duration: Duration ( seconds: 3 ) ,
content:
const Text ( ' New place added. ' , style: TextStyle ( fontSize: 16.0 ) ) ,
action: SnackBarAction (
label: ' Edit ' ,
onPressed: ( ) async {
_pushPlaceDetailsScreen ( newPlace ) ;
} ,
) ,
) ,
) ;
/ / Add the new place to the places stored in appState .
final newPlaces =
List < Place > . from ( Provider . of < AppState > ( context , listen: false ) . 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:
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
Provider . of < AppState > ( context , listen: false ) . setPlaces ( newPlaces ) ;
}
}
Future < Marker > _createPlaceMarker ( BuildContext context , Place place ) async {
final marker = Marker (
markerId: MarkerId ( place . latLng . toString ( ) ) ,
@ -100,18 +234,53 @@ class PlaceMapState extends State<PlaceMap> {
return marker ;
}
void _pushPlaceDetailsScreen ( Place place ) {
assert ( place ! = null ) ;
Future < void > _maybeUpdateMapConfiguration ( ) async {
_configuration ? ? =
MapConfiguration . of ( Provider . of < AppState > ( context , listen: false ) ) ;
final newConfiguration =
MapConfiguration . of ( Provider . of < AppState > ( context , listen: false ) ) ;
Navigator . push < void > (
context ,
MaterialPageRoute ( builder: ( context ) {
return PlaceDetails (
place: place ,
onChanged: ( value ) = > _onPlaceChanged ( value ) ,
/ / Since we manually update [ _configuration ] when place or selectedCategory
/ / changes come from the [ place_map ] , we should only enter this if statement
/ / when returning to the [ place_map ] after changes have been made from
/ / [ place_list ] .
if ( _configuration ! = newConfiguration & & mapController ! = null ) {
if ( _configuration . places = = newConfiguration . places & &
_configuration . selectedCategory ! =
newConfiguration . selectedCategory ) {
/ / If the configuration change is only a category change , just update
/ / the marker visibilities .
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 .
newConfiguration . places
. where ( ( p ) = > ! _configuration . places . contains ( p ) )
. map ( ( value ) = > _updateExistingPlaceMarker ( place: value ) ) ;
await _zoomToFitPlaces (
_getPlacesForCategory (
newConfiguration . selectedCategory ,
newConfiguration . places ,
) ,
) ;
} ) ,
}
_configuration = newConfiguration ;
}
}
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 ;
} ) ;
}
void _onPlaceChanged ( Place value ) {
@ -135,37 +304,27 @@ class PlaceMapState extends State<PlaceMap> {
Provider . of < AppState > ( context , listen: false ) . setPlaces ( newPlaces ) ;
}
void _ updateExistingPlaceMarker( { @ required Place place } ) {
var marker = _markedPlaces . keys
. singleWhere ( ( value ) = > _markedPlaces [ value ] . id = = place . id ) ;
void _ onToggleMapTypePressed( ) {
final nextType =
MapType . values [ ( _currentMapType . index + 1 ) % MapType . values . length ] ;
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 ) ;
_currentMapType = nextType ;
} ) ;
}
void _updateMarker ( {
@ required Marker marker ,
@ required Marker updatedMarker ,
@ required Place place ,
} ) {
_markers . remove ( marker ) ;
_markedPlaces . remove ( marker ) ;
_markers . add ( updatedMarker ) ;
_markedPlaces [ updatedMarker ] = place ;
}
void _pushPlaceDetailsScreen ( Place place ) {
assert ( place ! = null ) ;
Future < void > _switchSelectedCategory ( PlaceCategory category ) async {
Provider . of < AppState > ( context , listen: false ) . setSelectedCategory ( category ) ;
await _showPlacesForSelectedCategory ( category ) ;
Navigator . push < void > (
context ,
MaterialPageRoute ( builder: ( context ) {
return PlaceDetails (
place: place ,
onChanged: ( value ) = > _onPlaceChanged ( value ) ,
) ;
} ) ,
) ;
}
Future < void > _showPlacesForSelectedCategory ( PlaceCategory category ) async {
@ -190,213 +349,142 @@ class PlaceMapState extends State<PlaceMap> {
) ) ;
}
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 ,
) ,
) ;
}
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 > _switchSelectedCategory ( PlaceCategory category ) async {
Provider . of < AppState > ( context , listen: false ) . setSelectedCategory ( category ) ;
await _showPlacesForSelectedCategory ( category ) ;
}
Future < void > _confirmAddPlace ( BuildContext context ) async {
if ( _pendingMarker ! = null ) {
/ / Create a new Place and map it to the marker we just added .
final newPlace = Place (
id: Uuid ( ) . v1 ( ) ,
latLng: _pendingMarker . position ,
name: _pendingMarker . infoWindow . title ,
category:
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
var placeMarker = await _getPlaceMarkerIcon ( context ,
Provider . of < AppState > ( context , listen: false ) . selectedCategory ) ;
setState ( ( ) {
final updatedMarker = _pendingMarker . copyWith (
iconParam: placeMarker ,
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 (
SnackBar (
duration: Duration ( seconds: 3 ) ,
content:
const Text ( ' New place added. ' , style: TextStyle ( fontSize: 16.0 ) ) ,
action: SnackBarAction (
label: ' Edit ' ,
onPressed: ( ) async {
_pushPlaceDetailsScreen ( newPlace ) ;
} ,
) ,
) ,
) ;
/ / Add the new place to the places stored in appState .
final newPlaces =
List < Place > . from ( Provider . of < AppState > ( context , listen: false ) . 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:
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 ) ;
void _cancelAddPlace ( ) {
if ( _pendingMarker ! = null ) {
setState ( ( ) {
_markers . remove ( _pendingMarker ) ;
_pendingMarker = null ;
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 _onToggleMapTypePressed ( ) {
final nextType =
MapType . values [ ( _currentMapType . index + 1 ) % MapType . values . length ] ;
void _updateMarker ( {
@ required Marker marker ,
@ required Marker updatedMarker ,
@ required Place place ,
} ) {
_markers . remove ( marker ) ;
_markedPlaces . remove ( marker ) ;
setState ( ( ) {
_currentMapType = nextType ;
} ) ;
_markers . add ( updatedMarker ) ;
_markedPlaces [ updatedMarker ] = place ;
}
Future < void > _maybeUpdateMapConfiguration ( ) async {
_configuration ? ? =
MapConfiguration . of ( Provider . of < AppState > ( context , listen: false ) ) ;
final newConfiguration =
MapConfiguration . of ( Provider . of < AppState > ( context , listen: false ) ) ;
Future < void > _zoomToFitPlaces ( List < Place > places ) async {
var controller = await mapController . future ;
/ / Since we manually update [ _configuration ] when place or selectedCategory
/ / changes come from the [ place_map ] , we should only enter this if statement
/ / when returning to the [ place_map ] after changes have been made from
/ / [ place_list ] .
if ( _configuration ! = newConfiguration & & mapController ! = null ) {
if ( _configuration . places = = newConfiguration . places & &
_configuration . selectedCategory ! =
newConfiguration . selectedCategory ) {
/ / If the configuration change is only a category change , just update
/ / the marker visibilities .
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 .
newConfiguration . places
. where ( ( p ) = > ! _configuration . places . contains ( p ) )
. map ( ( value ) = > _updateExistingPlaceMarker ( place: value ) ) ;
/ / 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 ;
await _zoomToFitPlaces (
_getPlacesForCategory (
newConfiguration . selectedCategory ,
newConfiguration . places ,
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 ,
) ,
) ;
}
_configuration = newConfiguration ;
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 _AddPlaceButtonBar extends StatelessWidget {
final bool visible ;
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 ) {
_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: < Widget > [
GoogleMap (
onMapCreated: onMapCreated ,
initialCameraPosition: CameraPosition (
target: widget . center ,
zoom: 11.0 ,
) ,
mapType: _currentMapType ,
markers: _markers ,
onCameraMove: ( position ) = > _lastMapPosition = position . target ,
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: Colors . blue ,
child: const Text (
' Save ' ,
style: TextStyle ( color: Colors . white , fontSize: 16.0 ) ,
) ,
_CategoryButtonBar (
selectedPlaceCategory: state . selectedCategory ,
visible: _pendingMarker = = null ,
onChanged: _switchSelectedCategory ,
onPressed: onSavePressed ,
) ,
_AddPlaceButtonBar (
visible: _pendingMarker ! = null ,
onSavePressed: ( ) = > _confirmAddPlace ( context ) ,
onCancelPressed: _cancelAddPlace ,
RaisedButton (
color: Colors . red ,
child: const Text (
' Cancel ' ,
style: TextStyle ( color: Colors . white , fontSize: 16.0 ) ,
) ,
_MapFabs (
visible: _pendingMarker = = null ,
onAddPlacePressed: _onAddPlacePressed ,
onToggleMapTypePressed: _onToggleMapTypePressed ,
onPressed: onCancelPressed ,
) ,
] ,
) ,
) ,
) ;
} ) ;
}
}
class _CategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedPlaceCategory ;
final bool visible ;
final ValueChanged < PlaceCategory > onChanged ;
const _CategoryButtonBar ( {
Key key ,
@ required this . selectedPlaceCategory ,
@ -407,10 +495,6 @@ class _CategoryButtonBar extends StatelessWidget {
assert ( onChanged ! = null ) ,
super ( key: key ) ;
final PlaceCategory selectedPlaceCategory ;
final bool visible ;
final ValueChanged < PlaceCategory > onChanged ;
@ override
Widget build ( BuildContext context ) {
return Visibility (
@ -420,7 +504,7 @@ class _CategoryButtonBar extends StatelessWidget {
alignment: Alignment . bottomCenter ,
child: ButtonBar (
alignment: MainAxisAlignment . center ,
children: < Widget > [
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 ) ;
class _MapFabs extends StatelessWidget {
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 ,
) ,
] ,
) ,
) ,
) ;
}
}
final VoidCallback onAddPlacePressed ;
final VoidCallback onToggleMapTypePressed ;
class _MapFabs extends StatelessWidget {
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 ,
) ;
}
}