mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
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.
This commit is contained in:
68
place_tracker/lib/app_model.dart
Normal file
68
place_tracker/lib/app_model.dart
Normal file
@@ -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,
|
||||||
|
|||||||
209
place_tracker/lib/place_list.dart
Normal file
209
place_tracker/lib/place_list.dart
Normal file
@@ -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,
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,3 +513,32 @@ 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
146
place_tracker/lib/place_tracker_app.dart
Normal file
146
place_tracker/lib/place_tracker_app.dart
Normal file
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user