Add a list view to the place tracker app. (#30)

* Add list view to place tracker. Note: map in listTile is not WAI in this commit.

* Remove map from list tiles. Make list tiles tappable (currently editing a place and saving will do nothing if the details screen is pushed from the list view.

* Fix text alignment in list.

* Initial implementation of using an InheritedWidget to maintain data between list and map. Map does not update correctly at this point.

* Use AppModel.update to set the AppState. Add MapConfiguration class to handle map changes based on AppState.

* Don't cache AppState - lookup directly. Extract AppState code into it's own file and add static methods. Address comments from Hans.

* Extract generic AppModel code.
pull/33/head
Kenzie Schmoll 6 years ago committed by GitHub
parent e59c865884
commit 79d9d143b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
class _AppModelScope<T> extends InheritedWidget {
const _AppModelScope({
Key key,
this.appModelState,
Widget child
}) : super(key: key, child: child);
final _AppModelState<T> appModelState;
@override
bool updateShouldNotify(_AppModelScope oldWidget) => true;
}
class AppModel<T> extends StatefulWidget {
AppModel({
Key key,
@required this.initialState,
this.child,
}) : assert(initialState != null),
super(key: key);
final T initialState;
final Widget child;
_AppModelState<T> createState() => _AppModelState<T>();
static _typeOf<T>() => T;
static T of<T>(BuildContext context) {
final Type appModelScopeType = _typeOf<_AppModelScope<T>>();
final _AppModelScope<T> scope = context.inheritFromWidgetOfExactType(appModelScopeType);
return scope.appModelState.currentState;
}
static void update<T>(BuildContext context, T newState) {
final Type appModelScopeType = _typeOf<_AppModelScope<T>>();
final _AppModelScope<T> scope = context.inheritFromWidgetOfExactType(appModelScopeType);
scope.appModelState.updateState(newState);
}
}
class _AppModelState<T> extends State<AppModel<T>> {
@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<T>(
appModelState: this,
child: widget.child,
);
}
}

@ -1,24 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'place_map.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class _Home extends StatelessWidget { import 'place_tracker_app.dart';
const _Home({ Key key }) : super(key: key);
@override
Widget build(BuildContext context) {
return PlaceMap(
center: const LatLng(45.521563, -122.677433),
);
}
}
void main() { void main() {
runApp( runApp(PlaceTrackerApp());
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Place Tracker',
home: _Home(),
)
);
} }

@ -9,16 +9,19 @@ enum PlaceCategory {
class Place { class Place {
const Place({ const Place({
@required this.id,
@required this.latLng, @required this.latLng,
@required this.name, @required this.name,
@required this.category, @required this.category,
this.description, this.description,
this.starRating = 0, this.starRating = 0,
}) : assert(latLng != null), }) : assert(id != null),
assert(latLng != null),
assert(name != null), assert(name != null),
assert(category != null), assert(category != null),
assert(starRating != null && starRating >= 0 && starRating <= 5); assert(starRating != null && starRating >= 0 && starRating <= 5);
final String id;
final LatLng latLng; final LatLng latLng;
final String name; final String name;
final PlaceCategory category; final PlaceCategory category;
@ -29,6 +32,7 @@ class Place {
double get longitude => latLng.longitude; double get longitude => latLng.longitude;
Place copyWith({ Place copyWith({
String id,
LatLng latLng, LatLng latLng,
String name, String name,
PlaceCategory category, PlaceCategory category,
@ -36,6 +40,7 @@ class Place {
int starRating, int starRating,
}) { }) {
return Place( return Place(
id: id ?? this.id,
latLng: latLng ?? this.latLng, latLng: latLng ?? this.latLng,
name: name ?? this.name, name: name ?? this.name,
category: category ?? this.category, category: category ?? this.category,

@ -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<PlaceList> {
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<Place> 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: <Widget>[
_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<Place> 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: <Widget>[
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<PlaceCategory> onCategoryChanged;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_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<PlaceCategory> 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),
),
),
);
}
}

@ -2,10 +2,11 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:uuid/uuid.dart';
import 'place.dart'; import 'place.dart';
import 'stub_data.dart';
import 'place_details.dart'; import 'place_details.dart';
import 'place_tracker_app.dart';
class PlaceMap extends StatefulWidget { class PlaceMap extends StatefulWidget {
const PlaceMap({ const PlaceMap({
@ -35,30 +36,36 @@ class PlaceMapState extends State<PlaceMap> {
} }
} }
static List<Place> _getPlacesForCategory(PlaceCategory category, Map<Marker, Place> places) { static List<Place> _getPlacesForCategory(PlaceCategory category, List<Place> places) {
return places.values.where((Place place) => place.category == category).toList(); return places.where((Place place) => place.category == category).toList();
} }
GoogleMapController mapController; GoogleMapController mapController;
PlaceCategory _selectedPlaceCategory = PlaceCategory.favorite; Map<Marker, Place> _markedPlaces = Map<Marker, Place>();
Map<Marker, Place> _places = Map<Marker, Place>();
Marker _pendingMarker; Marker _pendingMarker;
MapConfiguration _configuration;
void onMapCreated(GoogleMapController controller) async { void onMapCreated(GoogleMapController controller) async {
mapController = controller; mapController = controller;
mapController.onInfoWindowTapped.add(_onInfoWindowTapped); mapController.onInfoWindowTapped.add(_onInfoWindowTapped);
// Add stub data on creation so we have something interesting to look at. // Draw initial place markers on creation so that we have something
final Map<Marker, Place> places = await _initializeStubPlaces(); // interesting to look at.
_zoomToFitPlaces(_getPlacesForCategory(_selectedPlaceCategory, places)); final Map<Marker, Place> places = await _markPlaces();
_zoomToFitPlaces(
_getPlacesForCategory(
AppState.of(context).selectedCategory,
places.values.toList(),
),
);
} }
Future<Map<Marker, Place>> _initializeStubPlaces() async { Future<Map<Marker, Place>> _markPlaces() async {
await Future.wait(StubData.places.map((Place place) => _initializeStubPlace(place))); await Future.wait(AppState.of(context).places.map((Place place) => _markPlace(place)));
return _places; return _markedPlaces;
} }
Future<void> _initializeStubPlace(Place place) async { Future<void> _markPlace(Place place) async {
final Marker marker = await mapController.addMarker( final Marker marker = await mapController.addMarker(
MarkerOptions( MarkerOptions(
position: place.latLng, position: place.latLng,
@ -67,34 +74,53 @@ class PlaceMapState extends State<PlaceMap> {
place.name, place.name,
'${place.starRating} Star Rating', '${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 { void _onInfoWindowTapped(Marker marker) {
_pushPlaceDetailsScreen(marker); _pushPlaceDetailsScreen(_markedPlaces[marker]);
} }
Future<void> _pushPlaceDetailsScreen(Marker marker) async { void _pushPlaceDetailsScreen(Place place) {
assert(marker != null); assert(place != null);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return PlaceDetails( return PlaceDetails(
place: _places[marker], place: place,
onChanged: (Place value) { onChanged: (Place value) => _onPlaceChanged(value),
_updatePlaceAndMarker(value, marker);
},
); );
}), }),
); );
} }
Future<void> _updatePlaceAndMarker(Place place, Marker marker) async { void _onPlaceChanged(Place value) {
_places[marker] = place; // Replace the place with the modified version.
final List<Place> 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 // Set marker visibility to false to ensure the info window is hidden. Once
// the plugin fully supports the Google Maps API, use hideInfoWindow() // the plugin fully supports the Google Maps API, use hideInfoWindow()
@ -111,31 +137,31 @@ class PlaceMapState extends State<PlaceMap> {
infoWindowText: InfoWindowText( infoWindowText: InfoWindowText(
place.name, place.name,
place.starRating != 0 place.starRating != 0
? '${place.starRating} Star Rating' ? '${place.starRating} Star Rating'
: null, : null,
), ),
visible: true, visible: true,
), ),
); );
_markedPlaces[marker] = place;
} }
void _updatePlaces(PlaceCategory category) { void _switchSelectedCategory(PlaceCategory category) {
setState(() { AppState.updateWith(context, selectedCategory: category);
_selectedPlaceCategory = category; _showPlacesForSelectedCategory(category);
_showPlacesForSelectedCategory();
});
} }
void _showPlacesForSelectedCategory() { void _showPlacesForSelectedCategory(PlaceCategory category) async {
_places.forEach((Marker marker, Place place) { await _markedPlaces.forEach((Marker marker, Place place) {
mapController.updateMarker( mapController.updateMarker(
marker, marker,
MarkerOptions( MarkerOptions(
visible: place.category == _selectedPlaceCategory, visible: place.category == category,
), ),
); );
}); });
_zoomToFitPlaces(_getPlacesForCategory(_selectedPlaceCategory, _places)); _zoomToFitPlaces(_getPlacesForCategory(category, _markedPlaces.values.toList()));
} }
void _zoomToFitPlaces(List<Place> places) { void _zoomToFitPlaces(List<Place> places) {
@ -184,16 +210,22 @@ class PlaceMapState extends State<PlaceMap> {
await mapController.updateMarker( await mapController.updateMarker(
_pendingMarker, _pendingMarker,
MarkerOptions( MarkerOptions(
icon: _getPlaceMarkerIcon(_selectedPlaceCategory), icon: _getPlaceMarkerIcon(AppState.of(context).selectedCategory),
infoWindowText: InfoWindowText('New Place', null), infoWindowText: InfoWindowText('New Place', null),
draggable: false, draggable: false,
), ),
); );
// Store a reference to the new marker so that we can pass it to the // Create a new Place and map it to the marker we just added.
// snackbar action. We cannot pass [_pendingMarker] since it will get final Place newPlace = Place(
// reset to null. id: Uuid().v1(),
Marker newMarker = _pendingMarker; 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( Scaffold.of(context).showSnackBar(
SnackBar( SnackBar(
duration: Duration(seconds: 3), duration: Duration(seconds: 3),
@ -204,19 +236,27 @@ class PlaceMapState extends State<PlaceMap> {
action: SnackBarAction( action: SnackBarAction(
label: 'Edit', label: 'Edit',
onPressed: () async { onPressed: () async {
_pushPlaceDetailsScreen(newMarker); _pushPlaceDetailsScreen(newPlace);
}, },
), ),
), ),
); );
// Add the new place to the places stored in appState.
final List<Place> 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(() { 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; _pendingMarker = null;
}); });
} }
@ -240,60 +280,80 @@ class PlaceMapState extends State<PlaceMap> {
); );
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( _maybeUpdateMapConfiguration();
appBar: AppBar(
title: Row( return Builder(builder: (BuildContext context) {
crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[
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],
),
// We need this additional builder here so that we can pass its context to // We need this additional builder here so that we can pass its context to
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a // _AddPlaceButtonBar's onSavePressed callback. This callback shows a
// SnackBar and to do this, we need a build context that has Scaffold as // SnackBar and to do this, we need a build context that has Scaffold as
// an ancestor. // an ancestor.
body: Builder(builder: (BuildContext context) { return Center(
return Center( child: Stack(
child: Stack( children: <Widget>[
children: <Widget>[ GoogleMap(
GoogleMap( onMapCreated: onMapCreated,
onMapCreated: onMapCreated, options: GoogleMapOptions(
options: GoogleMapOptions( trackCameraPosition: true,
trackCameraPosition: true, cameraPosition: CameraPosition(
cameraPosition: CameraPosition( target: widget.center,
target: widget.center, zoom: 11.0,
zoom: 11.0,
),
), ),
), ),
_CategoryButtonBar( ),
selectedPlaceCategory: _selectedPlaceCategory, _CategoryButtonBar(
visible: _pendingMarker == null, selectedPlaceCategory: AppState.of(context).selectedCategory,
onChanged: _updatePlaces, visible: _pendingMarker == null,
), onChanged: _switchSelectedCategory,
_AddPlaceButtonBar( ),
visible: _pendingMarker != null, _AddPlaceButtonBar(
onSavePressed: () => _confirmAddPlace(context), visible: _pendingMarker != null,
onCancelPressed: _cancelAddPlace, onSavePressed: () => _confirmAddPlace(context),
), onCancelPressed: _cancelAddPlace,
_MapFabs( ),
visible: _pendingMarker == null, _MapFabs(
onAddPlacePressed: _onAddPlacePressed, visible: _pendingMarker == null,
onToggleMapTypePressed: _onToggleMapTypePressed, onAddPlacePressed: _onAddPlacePressed,
), onToggleMapTypePressed: _onToggleMapTypePressed,
], ),
), ],
); ),
}), );
); });
} }
} }
@ -452,4 +512,33 @@ 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
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,
);
}
}

@ -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<PlaceTrackerApp> {
AppState appState = AppState();
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (BuildContext context, Widget child) {
return AppModel<AppState>(
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 <Widget>[
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: <Widget>[
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: <Widget>[
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<Place> places;
final PlaceCategory selectedCategory;
final PlaceTrackerViewType viewType;
AppState copyWith({
List<Place> 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<AppState>(context);
static void update(BuildContext context, AppState newState) {
AppModel.update<AppState>(context, newState);
}
static void updateWith(
BuildContext context,
{List<Place> 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);
}

@ -5,6 +5,7 @@ import 'place.dart';
class StubData { class StubData {
static const List<Place> places = [ static const List<Place> places = [
Place( Place(
id: '1',
latLng: LatLng(45.524676, -122.681922), latLng: LatLng(45.524676, -122.681922),
name: 'Deschutes Brewery', name: 'Deschutes Brewery',
description: description:
@ -13,6 +14,7 @@ class StubData {
starRating: 5, starRating: 5,
), ),
Place( Place(
id: '2',
latLng: LatLng(45.516887, -122.675417), latLng: LatLng(45.516887, -122.675417),
name: 'Luc Lac Vietnamese Kitchen', name: 'Luc Lac Vietnamese Kitchen',
description: 'Popular counter-serve offering pho, banh mi & other Vietnamese favorites in a stylish setting.', description: 'Popular counter-serve offering pho, banh mi & other Vietnamese favorites in a stylish setting.',
@ -20,6 +22,7 @@ class StubData {
starRating: 5, starRating: 5,
), ),
Place( Place(
id: '3',
latLng: LatLng(45.528952, -122.698344), latLng: LatLng(45.528952, -122.698344),
name: 'Salt & Straw', name: 'Salt & Straw',
description: description:
@ -28,6 +31,7 @@ class StubData {
starRating: 5, starRating: 5,
), ),
Place( Place(
id: '4',
latLng: LatLng(45.525253, -122.684423), latLng: LatLng(45.525253, -122.684423),
name: 'TILT', name: 'TILT',
description: description:
@ -36,6 +40,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '5',
latLng: LatLng(45.513485, -122.657982), latLng: LatLng(45.513485, -122.657982),
name: 'White Owl Social Club', name: 'White Owl Social Club',
description: description:
@ -44,6 +49,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '6',
latLng: LatLng(45.487137, -122.799940), latLng: LatLng(45.487137, -122.799940),
name: 'Buffalo Wild Wings', name: 'Buffalo Wild Wings',
description: description:
@ -52,6 +58,7 @@ class StubData {
starRating: 5, starRating: 5,
), ),
Place( Place(
id: '7',
latLng: LatLng(45.416986, -122.743171), latLng: LatLng(45.416986, -122.743171),
name: 'Chevys', name: 'Chevys',
description: description:
@ -60,6 +67,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '8',
latLng: LatLng(45.430489, -122.831802), latLng: LatLng(45.430489, -122.831802),
name: 'Cinetopia', name: 'Cinetopia',
description: description:
@ -68,6 +76,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '9',
latLng: LatLng(45.383030, -122.758372), latLng: LatLng(45.383030, -122.758372),
name: 'Thai Cuisine', name: 'Thai Cuisine',
description: description:
@ -76,6 +85,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '10',
latLng: LatLng(45.493321, -122.669330), latLng: LatLng(45.493321, -122.669330),
name: 'The Old Spaghetti Factory', name: 'The Old Spaghetti Factory',
description: description:
@ -84,6 +94,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '11',
latLng: LatLng(45.548606, -122.675286), latLng: LatLng(45.548606, -122.675286),
name: 'Mississippi Pizza', name: 'Mississippi Pizza',
description: description:
@ -92,6 +103,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '12',
latLng: LatLng(45.420226, -122.740347), latLng: LatLng(45.420226, -122.740347),
name: 'Oswego Grill', name: 'Oswego Grill',
description: description:
@ -100,6 +112,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '13',
latLng: LatLng(45.541202, -122.676432), latLng: LatLng(45.541202, -122.676432),
name: 'The Widmer Brothers Brewery', name: 'The Widmer Brothers Brewery',
description: description:
@ -108,6 +121,7 @@ class StubData {
starRating: 4, starRating: 4,
), ),
Place( Place(
id: '14',
latLng: LatLng(45.559783, -122.924103), latLng: LatLng(45.559783, -122.924103),
name: 'TopGolf', name: 'TopGolf',
description: description:
@ -116,6 +130,7 @@ class StubData {
starRating: 5, starRating: 5,
), ),
Place( Place(
id: '15',
latLng: LatLng(45.485612, -122.784733), latLng: LatLng(45.485612, -122.784733),
name: 'Uwajimaya Beaverton', name: 'Uwajimaya Beaverton',
description: 'Huge Asian grocery outpost stocking meats, produce & prepared foods plus gifts & home goods.', description: 'Huge Asian grocery outpost stocking meats, produce & prepared foods plus gifts & home goods.',

@ -17,6 +17,8 @@ dependencies:
url: git://github.com/flutter/plugins url: git://github.com/flutter/plugins
path: packages/google_maps_flutter path: packages/google_maps_flutter
uuid: 1.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

Loading…
Cancel
Save