diff --git a/place_tracker/lib/app_model.dart b/place_tracker/lib/app_model.dart new file mode 100644 index 000000000..e68d65dbd --- /dev/null +++ b/place_tracker/lib/app_model.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class _AppModelScope extends InheritedWidget { + const _AppModelScope({ + Key key, + this.appModelState, + Widget child + }) : super(key: key, child: child); + + final _AppModelState appModelState; + + @override + bool updateShouldNotify(_AppModelScope oldWidget) => true; +} + +class AppModel extends StatefulWidget { + AppModel({ + Key key, + @required this.initialState, + this.child, + }) : assert(initialState != null), + super(key: key); + + final T initialState; + final Widget child; + + _AppModelState createState() => _AppModelState(); + + static _typeOf() => T; + + static T of(BuildContext context) { + final Type appModelScopeType = _typeOf<_AppModelScope>(); + final _AppModelScope scope = context.inheritFromWidgetOfExactType(appModelScopeType); + return scope.appModelState.currentState; + } + + static void update(BuildContext context, T newState) { + final Type appModelScopeType = _typeOf<_AppModelScope>(); + final _AppModelScope scope = context.inheritFromWidgetOfExactType(appModelScopeType); + scope.appModelState.updateState(newState); + } +} + +class _AppModelState extends State> { + @override + void initState() { + super.initState(); + currentState = widget.initialState; + } + + T currentState; + + void updateState(T newState) { + if (newState != currentState) { + setState(() { + currentState = newState; + }); + } + } + + @override + Widget build(BuildContext context) { + return _AppModelScope( + appModelState: this, + child: widget.child, + ); + } +} diff --git a/place_tracker/lib/main.dart b/place_tracker/lib/main.dart index ba1426c55..197539606 100644 --- a/place_tracker/lib/main.dart +++ b/place_tracker/lib/main.dart @@ -1,24 +1,7 @@ import 'package:flutter/material.dart'; -import 'place_map.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -class _Home extends StatelessWidget { - const _Home({ Key key }) : super(key: key); - - @override - Widget build(BuildContext context) { - return PlaceMap( - center: const LatLng(45.521563, -122.677433), - ); - } -} +import 'place_tracker_app.dart'; void main() { - runApp( - MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Place Tracker', - home: _Home(), - ) - ); + runApp(PlaceTrackerApp()); } diff --git a/place_tracker/lib/place.dart b/place_tracker/lib/place.dart index 1068d96e5..05401ede5 100644 --- a/place_tracker/lib/place.dart +++ b/place_tracker/lib/place.dart @@ -9,16 +9,19 @@ enum PlaceCategory { class Place { const Place({ + @required this.id, @required this.latLng, @required this.name, @required this.category, this.description, this.starRating = 0, - }) : assert(latLng != null), + }) : assert(id != null), + assert(latLng != null), assert(name != null), assert(category != null), assert(starRating != null && starRating >= 0 && starRating <= 5); + final String id; final LatLng latLng; final String name; final PlaceCategory category; @@ -29,6 +32,7 @@ class Place { double get longitude => latLng.longitude; Place copyWith({ + String id, LatLng latLng, String name, PlaceCategory category, @@ -36,6 +40,7 @@ class Place { int starRating, }) { return Place( + id: id ?? this.id, latLng: latLng ?? this.latLng, name: name ?? this.name, category: category ?? this.category, diff --git a/place_tracker/lib/place_list.dart b/place_tracker/lib/place_list.dart new file mode 100644 index 000000000..0fd2fe6fd --- /dev/null +++ b/place_tracker/lib/place_list.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; + +import 'place.dart'; +import 'place_details.dart'; +import 'place_tracker_app.dart'; + +class PlaceList extends StatefulWidget { + const PlaceList({ Key key }) : super(key: key); + + @override + PlaceListState createState() => PlaceListState(); +} + +class PlaceListState extends State { + ScrollController _scrollController = ScrollController(); + + void _onCategoryChanged(PlaceCategory newCategory) { + _scrollController.jumpTo(0.0); + AppState.updateWith(context, selectedCategory: newCategory); + } + + void _onPlaceChanged(Place value) { + // Replace the place with the modified version. + final List newPlaces = List.from(AppState.of(context).places); + final int index = newPlaces.indexWhere((Place place) => place.id == value.id); + newPlaces[index] = value; + + AppState.updateWith(context, places: newPlaces); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _ListCategoryButtonBar( + selectedCategory: AppState.of(context).selectedCategory, + onCategoryChanged: (value) => _onCategoryChanged(value), + ), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), + controller: _scrollController, + shrinkWrap: true, + children: AppState.of(context).places + .where((Place place) => place.category == AppState.of(context).selectedCategory) + .map((Place place) => _PlaceListTile( + place: place, + onPlaceChanged: (Place value) => _onPlaceChanged(value), + ) + ).toList(), + ), + ), + ] + ); + } +} + +class _PlaceListTile extends StatelessWidget { + const _PlaceListTile({ + Key key, + @required this.place, + @required this.onPlaceChanged, + }) : assert(place != null), + assert(onPlaceChanged != null), + super(key: key); + + final Place place; + final ValueChanged onPlaceChanged; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return PlaceDetails( + place: place, + onChanged: (Place value) => onPlaceChanged(value), + ); + }), + ), + child: Container( + padding: EdgeInsets.only(top: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + place.name, + textAlign: TextAlign.left, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20.0, + ), + maxLines: 3, + ), + Row( + children: List.generate(5, (int index) { + return Icon(Icons.star, + size: 28.0, + color: place.starRating > index ? Colors.amber : Colors.grey[400], + ); + }).toList(), + ), + Text( + place.description != null ? place.description : '', + style: Theme.of(context).textTheme.subhead, + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 16.0), + Divider( + height: 2.0, + color: Colors.grey[700], + ), + ], + ), + ), + ); + } +} + +class _ListCategoryButtonBar extends StatelessWidget { + const _ListCategoryButtonBar({ + Key key, + @required this.selectedCategory, + @required this.onCategoryChanged, + }) : assert(selectedCategory != null), + assert(onCategoryChanged != null), + super(key: key); + + final PlaceCategory selectedCategory; + final ValueChanged onCategoryChanged; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _CategoryButton( + category: PlaceCategory.favorite, + selected: selectedCategory == PlaceCategory.favorite, + onCategoryChanged: onCategoryChanged, + ),_CategoryButton( + category: PlaceCategory.visited, + selected: selectedCategory == PlaceCategory.visited, + onCategoryChanged: onCategoryChanged, + ),_CategoryButton( + category: PlaceCategory.wantToGo, + selected: selectedCategory == PlaceCategory.wantToGo, + onCategoryChanged: onCategoryChanged, + ), + ], + ); + } +} + +class _CategoryButton extends StatelessWidget { + const _CategoryButton({ + Key key, + @required this.category, + @required this.selected, + @required this.onCategoryChanged, + }) : assert(category != null), + assert(selected != null), + super(key: key); + + final PlaceCategory category; + final bool selected; + final ValueChanged onCategoryChanged; + + @override + Widget build(BuildContext context) { + String _buttonText; + switch (category) { + case PlaceCategory.favorite: + _buttonText = 'Favorites'; + break; + case PlaceCategory.visited: + _buttonText = 'Visited'; + break; + case PlaceCategory.wantToGo: + _buttonText = 'Want To Go'; + } + + return Container( + margin: EdgeInsets.symmetric(vertical: 12.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: selected ? Colors.blue : Colors.transparent, + ), + ), + ), + child: ButtonTheme( + height: 50.0, + child: FlatButton( + child: Text( + _buttonText, + style: TextStyle( + fontSize: selected ? 20.0 : 18.0, + color: selected ? Colors.blue : Colors.black87, + ), + ), + onPressed: () => onCategoryChanged(category), + ), + ), + ); + } +} diff --git a/place_tracker/lib/place_map.dart b/place_tracker/lib/place_map.dart index 459a57ae6..700354364 100644 --- a/place_tracker/lib/place_map.dart +++ b/place_tracker/lib/place_map.dart @@ -2,10 +2,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:uuid/uuid.dart'; import 'place.dart'; -import 'stub_data.dart'; import 'place_details.dart'; +import 'place_tracker_app.dart'; class PlaceMap extends StatefulWidget { const PlaceMap({ @@ -35,30 +36,36 @@ class PlaceMapState extends State { } } - static List _getPlacesForCategory(PlaceCategory category, Map places) { - return places.values.where((Place place) => place.category == category).toList(); + static List _getPlacesForCategory(PlaceCategory category, List places) { + return places.where((Place place) => place.category == category).toList(); } GoogleMapController mapController; - PlaceCategory _selectedPlaceCategory = PlaceCategory.favorite; - Map _places = Map(); + Map _markedPlaces = Map(); Marker _pendingMarker; + MapConfiguration _configuration; void onMapCreated(GoogleMapController controller) async { mapController = controller; mapController.onInfoWindowTapped.add(_onInfoWindowTapped); - // Add stub data on creation so we have something interesting to look at. - final Map places = await _initializeStubPlaces(); - _zoomToFitPlaces(_getPlacesForCategory(_selectedPlaceCategory, places)); + // Draw initial place markers on creation so that we have something + // interesting to look at. + final Map places = await _markPlaces(); + _zoomToFitPlaces( + _getPlacesForCategory( + AppState.of(context).selectedCategory, + places.values.toList(), + ), + ); } - Future> _initializeStubPlaces() async { - await Future.wait(StubData.places.map((Place place) => _initializeStubPlace(place))); - return _places; + Future> _markPlaces() async { + await Future.wait(AppState.of(context).places.map((Place place) => _markPlace(place))); + return _markedPlaces; } - Future _initializeStubPlace(Place place) async { + Future _markPlace(Place place) async { final Marker marker = await mapController.addMarker( MarkerOptions( position: place.latLng, @@ -67,34 +74,53 @@ class PlaceMapState extends State { place.name, '${place.starRating} Star Rating', ), - visible: place.category == _selectedPlaceCategory, + visible: place.category == AppState.of(context).selectedCategory, ), ); - _places[marker] = place; + _markedPlaces[marker] = place; } - void _onInfoWindowTapped(Marker marker) async { - _pushPlaceDetailsScreen(marker); + void _onInfoWindowTapped(Marker marker) { + _pushPlaceDetailsScreen(_markedPlaces[marker]); } - Future _pushPlaceDetailsScreen(Marker marker) async { - assert(marker != null); + void _pushPlaceDetailsScreen(Place place) { + assert(place != null); Navigator.push( context, MaterialPageRoute(builder: (context) { return PlaceDetails( - place: _places[marker], - onChanged: (Place value) { - _updatePlaceAndMarker(value, marker); - }, + place: place, + onChanged: (Place value) => _onPlaceChanged(value), ); }), ); } - Future _updatePlaceAndMarker(Place place, Marker marker) async { - _places[marker] = place; + void _onPlaceChanged(Place value) { + // Replace the place with the modified version. + final List newPlaces = List.from(AppState.of(context).places); + final int index = newPlaces.indexWhere((Place 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: AppState.of(context).selectedCategory, + ); + + AppState.updateWith(context, places: newPlaces); + } + + void _updateExistingPlaceMarker({@required Place place}) async { + 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() @@ -111,31 +137,31 @@ class PlaceMapState extends State { infoWindowText: InfoWindowText( place.name, place.starRating != 0 - ? '${place.starRating} Star Rating' - : null, + ? '${place.starRating} Star Rating' + : null, ), visible: true, ), ); + + _markedPlaces[marker] = place; } - void _updatePlaces(PlaceCategory category) { - setState(() { - _selectedPlaceCategory = category; - _showPlacesForSelectedCategory(); - }); + void _switchSelectedCategory(PlaceCategory category) { + AppState.updateWith(context, selectedCategory: category); + _showPlacesForSelectedCategory(category); } - void _showPlacesForSelectedCategory() { - _places.forEach((Marker marker, Place place) { + void _showPlacesForSelectedCategory(PlaceCategory category) async { + await _markedPlaces.forEach((Marker marker, Place place) { mapController.updateMarker( marker, MarkerOptions( - visible: place.category == _selectedPlaceCategory, + visible: place.category == category, ), ); }); - _zoomToFitPlaces(_getPlacesForCategory(_selectedPlaceCategory, _places)); + _zoomToFitPlaces(_getPlacesForCategory(category, _markedPlaces.values.toList())); } void _zoomToFitPlaces(List places) { @@ -184,16 +210,22 @@ class PlaceMapState extends State { await mapController.updateMarker( _pendingMarker, MarkerOptions( - icon: _getPlaceMarkerIcon(_selectedPlaceCategory), + icon: _getPlaceMarkerIcon(AppState.of(context).selectedCategory), infoWindowText: InfoWindowText('New Place', null), draggable: false, ), ); - // Store a reference to the new marker so that we can pass it to the - // snackbar action. We cannot pass [_pendingMarker] since it will get - // reset to null. - Marker newMarker = _pendingMarker; + // 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, + category: AppState.of(context).selectedCategory, + ); + _markedPlaces[_pendingMarker] = newPlace; + + // Show a confirmation snackbar that has an action to edit the new place. Scaffold.of(context).showSnackBar( SnackBar( duration: Duration(seconds: 3), @@ -204,19 +236,27 @@ class PlaceMapState extends State { action: SnackBarAction( label: 'Edit', onPressed: () async { - _pushPlaceDetailsScreen(newMarker); + _pushPlaceDetailsScreen(newPlace); }, ), ), ); + // Add the new place to the places stored in appState. + final List newPlaces = List.from(AppState.of(context).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.of(context).selectedCategory, + ); + + AppState.updateWith(context, places: newPlaces); + setState(() { - // Create a new Place and map it to the marker we just added. - _places[_pendingMarker] = Place( - latLng: _pendingMarker.options.position, - name: _pendingMarker.options.infoWindowText.title, - category: _selectedPlaceCategory, - ); _pendingMarker = null; }); } @@ -240,60 +280,80 @@ class PlaceMapState extends State { ); } + void _maybeUpdateMapConfiguration() async { + _configuration ??= MapConfiguration.of(AppState.of(context)); + final MapConfiguration newConfiguration = MapConfiguration.of(AppState.of(context)); + + // 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. + _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 newConfiguration.places.forEach((Place value) { + if (!_configuration.places.contains(value)) { + _updateExistingPlaceMarker(place: value); + } + }); + _zoomToFitPlaces( + _getPlacesForCategory( + newConfiguration.selectedCategory, + newConfiguration.places, + ), + ); + } + _configuration = newConfiguration; + } + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: const [ - Padding( - padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), - child: Icon(Icons.pin_drop, size: 24.0), - ), - Text('Place Tracker'), - ], - ), - backgroundColor: Colors.green[700], - ), + _maybeUpdateMapConfiguration(); + + return Builder(builder: (BuildContext 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. - body: Builder(builder: (BuildContext context) { - return Center( - child: Stack( - children: [ - GoogleMap( - onMapCreated: onMapCreated, - options: GoogleMapOptions( - trackCameraPosition: true, - cameraPosition: CameraPosition( - target: widget.center, - zoom: 11.0, - ), + return Center( + child: Stack( + children: [ + GoogleMap( + onMapCreated: onMapCreated, + options: GoogleMapOptions( + trackCameraPosition: true, + cameraPosition: CameraPosition( + target: widget.center, + zoom: 11.0, ), ), - _CategoryButtonBar( - selectedPlaceCategory: _selectedPlaceCategory, - visible: _pendingMarker == null, - onChanged: _updatePlaces, - ), - _AddPlaceButtonBar( - visible: _pendingMarker != null, - onSavePressed: () => _confirmAddPlace(context), - onCancelPressed: _cancelAddPlace, - ), - _MapFabs( - visible: _pendingMarker == null, - onAddPlacePressed: _onAddPlacePressed, - onToggleMapTypePressed: _onToggleMapTypePressed, - ), - ], - ), - ); - }), - ); + ), + _CategoryButtonBar( + selectedPlaceCategory: AppState.of(context).selectedCategory, + visible: _pendingMarker == null, + onChanged: _switchSelectedCategory, + ), + _AddPlaceButtonBar( + visible: _pendingMarker != null, + onSavePressed: () => _confirmAddPlace(context), + onCancelPressed: _cancelAddPlace, + ), + _MapFabs( + visible: _pendingMarker == null, + onAddPlacePressed: _onAddPlacePressed, + onToggleMapTypePressed: _onToggleMapTypePressed, + ), + ], + ), + ); + }); } } @@ -452,4 +512,33 @@ class _MapFabs extends StatelessWidget { ), ); } -} \ No newline at end of file +} + +class MapConfiguration { + const MapConfiguration({ + @required this.places, + @required this.selectedCategory, + }) : assert(places != null), + assert(selectedCategory != null); + + final List places; + final PlaceCategory selectedCategory; + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final MapConfiguration otherConfiguration = other; + return otherConfiguration.places == places + && otherConfiguration.selectedCategory == selectedCategory; + } + + static MapConfiguration of(AppState appState) { + return MapConfiguration( + places: appState.places, + selectedCategory: appState.selectedCategory, + ); + } +} diff --git a/place_tracker/lib/place_tracker_app.dart b/place_tracker/lib/place_tracker_app.dart new file mode 100644 index 000000000..20e10b160 --- /dev/null +++ b/place_tracker/lib/place_tracker_app.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'app_model.dart'; +import 'place.dart'; +import 'place_list.dart'; +import 'place_map.dart'; +import 'stub_data.dart'; + +enum PlaceTrackerViewType { + map, + list, +} + +class PlaceTrackerApp extends StatefulWidget { + @override + _PlaceTrackerAppState createState() => _PlaceTrackerAppState(); +} + +class _PlaceTrackerAppState extends State { + AppState appState = AppState(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (BuildContext context, Widget child) { + return AppModel( + initialState: AppState(), + child: child, + ); + }, + home: _PlaceTrackerHomePage(), + ); + } +} + +class _PlaceTrackerHomePage extends StatelessWidget { + const _PlaceTrackerHomePage({ Key key }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: Icon(Icons.pin_drop, size: 24.0), + ), + Text('Place Tracker'), + ], + ), + backgroundColor: Colors.green[700], + actions: [ + Padding( + padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0), + child: IconButton( + icon: Icon( + AppState.of(context).viewType == PlaceTrackerViewType.map + ? Icons.list + : Icons.map, + size: 32.0 + ), + onPressed: () { + AppState.updateWith( + context, + viewType: AppState.of(context).viewType == PlaceTrackerViewType.map + ? PlaceTrackerViewType.list + : PlaceTrackerViewType.map, + ); + }, + ), + ), + ], + ), + body: IndexedStack( + index: AppState.of(context).viewType == PlaceTrackerViewType.map ? 0 : 1, + children: [ + PlaceMap(center: const LatLng(45.521563, -122.677433)), + PlaceList(), + ], + ), + ); + } +} + +class AppState { + const AppState({ + this.places = StubData.places, + this.selectedCategory = PlaceCategory.favorite, + this.viewType = PlaceTrackerViewType.map, + }) : assert(places != null), + assert(selectedCategory != null); + + final List places; + final PlaceCategory selectedCategory; + final PlaceTrackerViewType viewType; + + AppState copyWith({ + List places, + PlaceCategory selectedCategory, + PlaceTrackerViewType viewType, + }) { + return AppState( + places: places ?? this.places, + selectedCategory: selectedCategory ?? this.selectedCategory, + viewType: viewType ?? this.viewType, + ); + } + + static AppState of(BuildContext context) => AppModel.of(context); + + static void update(BuildContext context, AppState newState) { + AppModel.update(context, newState); + } + + static void updateWith( + BuildContext context, + {List places, + PlaceCategory selectedCategory, + PlaceTrackerViewType viewType, + }) { + update( + context, + AppState.of(context).copyWith( + places: places, selectedCategory: selectedCategory, viewType: viewType, + ), + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final AppState otherAppState = other; + return otherAppState.places == places + && otherAppState.selectedCategory == selectedCategory + && otherAppState.viewType == viewType; + } + + @override + int get hashCode => hashValues(places, selectedCategory, viewType); +} diff --git a/place_tracker/lib/stub_data.dart b/place_tracker/lib/stub_data.dart index 82cf1ca25..360e5fe6a 100644 --- a/place_tracker/lib/stub_data.dart +++ b/place_tracker/lib/stub_data.dart @@ -5,6 +5,7 @@ import 'place.dart'; class StubData { static const List places = [ Place( + id: '1', latLng: LatLng(45.524676, -122.681922), name: 'Deschutes Brewery', description: @@ -13,6 +14,7 @@ class StubData { starRating: 5, ), Place( + id: '2', latLng: LatLng(45.516887, -122.675417), name: 'Luc Lac Vietnamese Kitchen', description: 'Popular counter-serve offering pho, banh mi & other Vietnamese favorites in a stylish setting.', @@ -20,6 +22,7 @@ class StubData { starRating: 5, ), Place( + id: '3', latLng: LatLng(45.528952, -122.698344), name: 'Salt & Straw', description: @@ -28,6 +31,7 @@ class StubData { starRating: 5, ), Place( + id: '4', latLng: LatLng(45.525253, -122.684423), name: 'TILT', description: @@ -36,6 +40,7 @@ class StubData { starRating: 4, ), Place( + id: '5', latLng: LatLng(45.513485, -122.657982), name: 'White Owl Social Club', description: @@ -44,6 +49,7 @@ class StubData { starRating: 4, ), Place( + id: '6', latLng: LatLng(45.487137, -122.799940), name: 'Buffalo Wild Wings', description: @@ -52,6 +58,7 @@ class StubData { starRating: 5, ), Place( + id: '7', latLng: LatLng(45.416986, -122.743171), name: 'Chevys', description: @@ -60,6 +67,7 @@ class StubData { starRating: 4, ), Place( + id: '8', latLng: LatLng(45.430489, -122.831802), name: 'Cinetopia', description: @@ -68,6 +76,7 @@ class StubData { starRating: 4, ), Place( + id: '9', latLng: LatLng(45.383030, -122.758372), name: 'Thai Cuisine', description: @@ -76,6 +85,7 @@ class StubData { starRating: 4, ), Place( + id: '10', latLng: LatLng(45.493321, -122.669330), name: 'The Old Spaghetti Factory', description: @@ -84,6 +94,7 @@ class StubData { starRating: 4, ), Place( + id: '11', latLng: LatLng(45.548606, -122.675286), name: 'Mississippi Pizza', description: @@ -92,6 +103,7 @@ class StubData { starRating: 4, ), Place( + id: '12', latLng: LatLng(45.420226, -122.740347), name: 'Oswego Grill', description: @@ -100,6 +112,7 @@ class StubData { starRating: 4, ), Place( + id: '13', latLng: LatLng(45.541202, -122.676432), name: 'The Widmer Brothers Brewery', description: @@ -108,6 +121,7 @@ class StubData { starRating: 4, ), Place( + id: '14', latLng: LatLng(45.559783, -122.924103), name: 'TopGolf', description: @@ -116,6 +130,7 @@ class StubData { starRating: 5, ), Place( + id: '15', latLng: LatLng(45.485612, -122.784733), name: 'Uwajimaya Beaverton', description: 'Huge Asian grocery outpost stocking meats, produce & prepared foods plus gifts & home goods.', diff --git a/place_tracker/pubspec.yaml b/place_tracker/pubspec.yaml index a893fb582..bdb66d854 100644 --- a/place_tracker/pubspec.yaml +++ b/place_tracker/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: url: git://github.com/flutter/plugins path: packages/google_maps_flutter + uuid: 1.0.3 + dev_dependencies: flutter_test: sdk: flutter