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

pull/30/head
kenzieschmoll 8 years ago
parent d6f02e7547
commit e42a53a716

@ -5,33 +5,26 @@ import 'place_details.dart';
import 'place_tracker_app.dart';
class PlaceList extends StatelessWidget {
PlaceList({
@required this.onChanged,
Key key,
}) : assert(onChanged != null),
super(key: key);
final ValueChanged<AppState> onChanged;
ScrollController _scrollController = ScrollController();
AppState appState;
void _onCategoryChanged(PlaceCategory newCategory) {
void _onCategoryChanged(PlaceCategory newCategory, BuildContext context) {
_scrollController.jumpTo(0.0);
onChanged(AppState(
AppModel.update<AppState>(context, AppState(
places: appState.places,
selectedCategory: newCategory,
viewType: appState.viewType,
));
}
void _onPlaceChanged(Place value) {
void _onPlaceChanged(Place value, BuildContext context) {
// Remove the old place and add the updated one.
List<Place> newPlaces = List.from(appState.places);
int index = newPlaces.indexWhere((Place place) => place.id == value.id);
newPlaces[index] = value;
onChanged(AppState(
AppModel.update<AppState>(context, AppState(
places: newPlaces,
selectedCategory: appState.selectedCategory,
viewType: appState.viewType,
@ -40,12 +33,12 @@ class PlaceList extends StatelessWidget {
@override
Widget build(BuildContext context) {
appState = AppState.of(context);
appState = AppModel.of<AppState>(context);
return Column(
children: <Widget>[
_ListCategoryButtonBar(
selectedCategory: appState.selectedCategory,
onCategoryChanged: (value) => _onCategoryChanged(value),
onCategoryChanged: (value) => _onCategoryChanged(value, context),
),
Expanded(
child: ListView(
@ -56,7 +49,7 @@ class PlaceList extends StatelessWidget {
.where((Place place) => place.category == appState.selectedCategory)
.map((Place place) => _PlaceListTile(
place: place,
onPlaceChanged: (Place value) => _onPlaceChanged(value),
onPlaceChanged: (Place value) => _onPlaceChanged(value, context),
)
).toList(),
),

@ -12,11 +12,9 @@ class PlaceMap extends StatefulWidget {
const PlaceMap({
Key key,
this.center,
this.onChanged,
}) : super(key: key);
final LatLng center;
final ValueChanged<AppState> onChanged;
@override
PlaceMapState createState() => PlaceMapState();
@ -48,7 +46,8 @@ class PlaceMapState extends State<PlaceMap> {
GoogleMapController mapController;
Map<Marker, Place> _markedPlaces = Map<Marker, Place>();
Marker _pendingMarker;
AppState appState;
AppState _appState;
MapConfiguration _configuration;
void onMapCreated(GoogleMapController controller) async {
mapController = controller;
@ -56,14 +55,12 @@ class PlaceMapState extends State<PlaceMap> {
// Draw initial place markers on creation so that we have something
// interesting to look at.
final Map<Marker, Place> places = await _markPlaces(appState.places);
_zoomToFitPlaces(_getPlacesForCategory(appState.selectedCategory, places.values.toList()));
final Map<Marker, Place> places = await _markPlaces();
_zoomToFitPlaces(_getPlacesForCategory(_appState.selectedCategory, places.values.toList()));
}
Future<Map<Marker, Place>> _markPlaces(List<Place> places) async {
await mapController.clearMarkers();
_markedPlaces.clear();
await Future.wait(places.map((Place place) => _markPlace(place)));
Future<Map<Marker, Place>> _markPlaces() async {
await Future.wait(_appState.places.map((Place place) => _markPlace(place)));
return _markedPlaces;
}
@ -76,56 +73,94 @@ class PlaceMapState extends State<PlaceMap> {
place.name,
'${place.starRating} Star Rating',
),
visible: place.category == appState.selectedCategory,
visible: place.category == _appState.selectedCategory,
),
);
_markedPlaces[marker] = place;
}
void _onInfoWindowTapped(Marker marker) async {
_pushPlaceDetailsScreen(marker);
void _onInfoWindowTapped(Marker marker) {
_pushPlaceDetailsScreen(_markedPlaces[marker]);
}
Future<void> _pushPlaceDetailsScreen(Marker marker) async {
assert(marker != null);
void _pushPlaceDetailsScreen(Place place) {
assert(place != null);
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: _markedPlaces[marker],
place: place,
onChanged: (Place value) => _onPlaceChanged(value),
);
}),
);
}
void _onPlaceChanged(Place value) async {
// Remove the old place and add the updated one.
List<Place> newPlaces = List.from(appState.places);
void _onPlaceChanged(Place value) {
// Replace the place in the AppState list with the modified one.
List<Place> newPlaces = List.from(_appState.places);
int index = newPlaces.indexWhere((Place place) => place.id == value.id);
newPlaces[index] = value;
widget.onChanged(AppState(
_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.selectedCategory,
viewType: appState.viewType,
));
selectedCategory: _appState.selectedCategory,
);
_markPlaces(newPlaces);
AppModel.update<AppState>(context, AppState(
places: newPlaces,
selectedCategory: _appState.selectedCategory,
viewType: _appState.viewType,
));
}
Future<void> updateMarkerVisibility() {
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()
// instead.
await mapController.updateMarker(
marker,
MarkerOptions(
visible: false,
),
);
await mapController.updateMarker(
marker,
MarkerOptions(
infoWindowText: InfoWindowText(
place.name,
place.starRating != 0
? '${place.starRating} Star Rating'
: null,
),
visible: true,
),
);
_markedPlaces[marker] = place;
}
void _switchSelectedCategory(PlaceCategory category) async {
widget.onChanged(AppState(
places: appState.places,
void _switchSelectedCategory(PlaceCategory category) {
AppModel.update<AppState>(context, AppState(
places: _appState.places,
selectedCategory: category,
viewType: appState.viewType,
viewType: _appState.viewType,
));
_showPlacesForSelectedCategory(category);
}
void _showPlacesForSelectedCategory(PlaceCategory category) async {
await _markedPlaces.forEach((Marker marker, Place place) {
mapController.updateMarker(
marker,
@ -183,16 +218,22 @@ class PlaceMapState extends State<PlaceMap> {
await mapController.updateMarker(
_pendingMarker,
MarkerOptions(
icon: _getPlaceMarkerIcon(appState.selectedCategory),
icon: _getPlaceMarkerIcon(_appState.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.
Place newPlace = Place(
id: Uuid().v1(),
latLng: _pendingMarker.options.position,
name: _pendingMarker.options.infoWindowText.title,
category: _appState.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),
@ -203,29 +244,28 @@ class PlaceMapState extends State<PlaceMap> {
action: SnackBarAction(
label: 'Edit',
onPressed: () async {
_pushPlaceDetailsScreen(newMarker);
_pushPlaceDetailsScreen(newPlace);
},
),
),
);
// Create a new Place and map it to the marker we just added.
Place newPlace = Place(
id: Uuid().v1(),
latLng: _pendingMarker.options.position,
name: _pendingMarker.options.infoWindowText.title,
category: appState.selectedCategory,
);
_markedPlaces[newMarker] = newPlace;
// Add the new place to the places stored in appState.
List<Place> newPlaces = List.from(appState.places)
List<Place> newPlaces = List.from(_appState.places)
..add(newPlace);
widget.onChanged(AppState(
// 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.selectedCategory,
viewType: appState.viewType,
selectedCategory: _appState.selectedCategory,
);
AppModel.update<AppState>(context, AppState(
places: newPlaces,
selectedCategory: _appState.selectedCategory,
viewType: _appState.viewType,
));
setState(() {
@ -252,14 +292,45 @@ class PlaceMapState extends State<PlaceMap> {
);
}
Future<void> redrawForCategory(PlaceCategory category) async {
final Map<Marker, Place> places = await _markPlaces(appState.places);
_zoomToFitPlaces(_getPlacesForCategory(category, places.values.toList()));
void _maybeUpdateMapConfiguration() async {
_configuration ??= MapConfiguration.of(_appState);
MapConfiguration newConfiguration = MapConfiguration.of(_appState);
// 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) {
appState = AppState.of(context);
_appState = AppModel.of<AppState>(context);
_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
@ -279,7 +350,7 @@ class PlaceMapState extends State<PlaceMap> {
),
),
_CategoryButtonBar(
selectedPlaceCategory: appState.selectedCategory,
selectedPlaceCategory: _appState.selectedCategory,
visible: _pendingMarker == null,
onChanged: _switchSelectedCategory,
),
@ -456,3 +527,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,
);
}
}

@ -23,34 +23,22 @@ class _PlaceTrackerAppState extends State<PlaceTrackerApp> {
Widget build(BuildContext context) {
return MaterialApp(
builder: (BuildContext context, Widget child) {
return PlaceTrackerAppModel(
state: appState,
return AppModel<AppState>(
initialState: AppState(),
child: child
);
},
home: _PlaceTrackerHomePage(
onChanged: (AppState newAppState) {
setState(() {
appState = newAppState;
});
},
),
home: _PlaceTrackerHomePage(),
);
}
}
class _PlaceTrackerHomePage extends StatelessWidget {
const _PlaceTrackerHomePage({
Key key,
this.onChanged,
}) : assert(onChanged != null),
super(key: key);
final ValueChanged<AppState> onChanged;
const _PlaceTrackerHomePage({ Key key }) : super(key: key);
@override
Widget build(BuildContext context) {
final AppState appState = AppState.of(context);
final AppState appState = AppModel.of<AppState>(context);
return Scaffold(
appBar: AppBar(
title: Row(
@ -75,8 +63,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
size: 32.0
),
onPressed: () {
onChanged(
AppState(
AppModel.update<AppState>(context, AppState(
places: appState.places,
selectedCategory: appState.selectedCategory,
viewType: appState.viewType == PlaceTrackerViewType.map
@ -92,13 +79,8 @@ class _PlaceTrackerHomePage extends StatelessWidget {
body: IndexedStack(
index: appState.viewType == PlaceTrackerViewType.map ? 0 : 1,
children: <Widget>[
PlaceMap(
center: const LatLng(45.521563, -122.677433),
onChanged: (AppState value) => onChanged(value),
),
PlaceList(
onChanged: (AppState value) => onChanged(value),
),
PlaceMap(center: const LatLng(45.521563, -122.677433)),
PlaceList(),
],
),
);
@ -131,23 +113,69 @@ class AppState {
@override
int get hashCode => places.hashCode;
}
static AppState of(BuildContext context) {
final PlaceTrackerAppModel model = context.inheritFromWidgetOfExactType(PlaceTrackerAppModel);
return model.state;
}
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 PlaceTrackerAppModel extends InheritedWidget {
PlaceTrackerAppModel({
class AppModel<T> extends StatefulWidget {
AppModel({
Key key,
this.state = const AppState(),
Widget child,
}) : assert(state != null),
super(key: key, child: child);
@required this.initialState,
this.child,
}) : assert(initialState != null),
super(key: key);
final T initialState;
final Widget child;
final AppState state;
_AppModelState<T> createState() => _AppModelState<T>();
static T of<T>(BuildContext context) {
final ofType = _AppModelScope<T>();
final _AppModelScope<dynamic> scope = context.inheritFromWidgetOfExactType(ofType.runtimeType);
return scope.appModelState.currentState;
}
static void update<T>(BuildContext context, T newState) {
final ofType = _AppModelScope<T>();
final _AppModelScope<dynamic> scope = context.inheritFromWidgetOfExactType(ofType.runtimeType);
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
bool updateShouldNotify(PlaceTrackerAppModel oldWidget) => state != oldWidget.state;
Widget build(BuildContext context) {
return _AppModelScope<T>(
appModelState: this,
child: widget.child,
);
}
}

Loading…
Cancel
Save