Place tracker/maintenance (#519)

* format place_tracker README

* update sentence in README

* sort Dart members, remove unnecessary type declarations

* Run place_tracker on Android and iOS, update project files

* add link to developer preview caveat

* grammar

* update MAINTENANCE
pull/522/head
John Ryan 4 years ago committed by GitHub
parent 00d0cdf02c
commit 437d1f620b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,7 +12,7 @@ match any new language/SDK features, etc.).
| infinite_list | filiph | 5/13/20 |
| isolate_example | johnpryan | 11/21/19 |
| jsonexample | redbrogdon | 1/3/20 |
| place_tracker | | |
| place_tracker | johnpryan | 8/13/20 |
| platform_channels | | |
| platform_design | johnpryan | 10/7/19 |
| platform_view_swift | redbrogdon | 10/7/19 |

@ -17,14 +17,16 @@ of building it out. This sample currently only works on Android (see Caveat belo
### `place_map.dart`
This page shows a full-screen GoogleMap widget with place markers. Provides examples of how
to stack other widgets on top of a GoogleMap widget, how to add markers to a map, and how to make
other flutter widgets interact with the GoogleMap widget.
This page shows a full-screen GoogleMap widget with place markers. Provides
examples of how to stack other widgets on top of a GoogleMap widget, how to add
markers to a map, and how to make other flutter widgets interact with the
GoogleMap widget.
### `place_details.dart`
This page shows a detailed view of a single place. Provides examples of how to place a
GoogleMap widget inside of a ListView and how to disable certain touch gestures on the map.
This page shows a detailed view of a single place. Provides examples of how to
place a GoogleMap widget inside of a ListView and how to disable certain touch
gestures on the map.
## Getting Started
@ -32,7 +34,8 @@ To run this sample app, you will need an API key.
Get an API key at <https://cloud.google.com/maps-platform/>.
Specify your API key in the application manifest `android/app/src/main/AndroidManifest.xml`:
Specify your API key in the application manifest
`android/app/src/main/AndroidManifest.xml`:
```xml
<manifest ...
@ -41,14 +44,17 @@ Specify your API key in the application manifest `android/app/src/main/AndroidMa
android:value="YOUR KEY HERE"/>
```
For additional help setting up the plugin, see the plugin's [README](https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter) page.
For additional help setting up the plugin, see the plugin's
[README](https://pub.dev/packages/google_maps_flutter)
page.
For help getting started with Flutter, view our online
[documentation](https://flutter.io/).
## Caveat
The google_maps_flutter plugin provides an *unpublished preview* of the Flutter API for Google Maps:
The google_maps_flutter plugin is in developer preview until [dynamic thread
merging](https://github.com/flutter/flutter/projects/155) is finished.
* Dart APIs for controlling and interacting with a GoogleMap view from Flutter
code are still being consolidated and expanded. The intention is to grow
current coverage into a complete offering. Issues and pull requests aimed to

@ -10,81 +10,32 @@ project 'Runner', {
'Release' => :release,
}
def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end
end
generated_key_values
end
target 'Runner' do
use_frameworks!
use_modular_headers!
# Flutter Pod
copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
# Plugin Pods
flutter_ios_podfile_setup
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
end
target 'Runner' do
use_frameworks!
use_modular_headers!
# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
install! 'cocoapods', :disable_input_output_paths => true
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
flutter_additional_ios_build_settings(target)
end
end

@ -9,11 +9,8 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
60896A287CEE44DA12929F37 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AC813E96EEED8049372B007 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -26,8 +23,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -37,14 +32,16 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
23C21398A8F98F6248469BBA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
28467FEB0EFFEE00368A5568 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
60CCDC9F87BA8463D14D1899 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8AC813E96EEED8049372B007 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -57,20 +54,36 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
60896A287CEE44DA12929F37 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
62C8D3D83EF1F84916A2F93E /* Frameworks */ = {
isa = PBXGroup;
children = (
8AC813E96EEED8049372B007 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
8B9122CDB85400D9D46FA60E /* Pods */ = {
isa = PBXGroup;
children = (
60CCDC9F87BA8463D14D1899 /* Pods-Runner.debug.xcconfig */,
23C21398A8F98F6248469BBA /* Pods-Runner.release.xcconfig */,
28467FEB0EFFEE00368A5568 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
@ -84,6 +97,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
8B9122CDB85400D9D46FA60E /* Pods */,
62C8D3D83EF1F84916A2F93E /* Frameworks */,
);
sourceTree = "<group>";
};
@ -125,12 +140,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B606037D737D7C74CC55B500 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
CAF79D024134EA0C14FDEDC4 /* [CP] Embed Pods Frameworks */,
2FBA60F1C5E1C14B292C896D /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -189,6 +207,24 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
2FBA60F1C5E1C14B292C896D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
"${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -201,7 +237,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@ -217,6 +253,46 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B606037D737D7C74CC55B500 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
CAF79D024134EA0C14FDEDC4 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/../Flutter/Flutter.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -253,7 +329,6 @@
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -330,7 +405,6 @@
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -386,7 +460,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
enum PlaceCategory {
favorite,
visited,
wantToGo,
}
class Place {
final String id;
final LatLng latLng;
final String name;
final PlaceCategory category;
final String description;
final int starRating;
const Place({
@required this.id,
@required this.latLng,
@ -21,13 +22,6 @@ class Place {
assert(category != null),
assert(starRating != null && starRating >= 0 && starRating <= 5);
final String id;
final LatLng latLng;
final String name;
final PlaceCategory category;
final String description;
final int starRating;
double get latitude => latLng.latitude;
double get longitude => latLng.longitude;
@ -50,3 +44,9 @@ class Place {
);
}
}
enum PlaceCategory {
favorite,
visited,
wantToGo,
}

@ -7,6 +7,9 @@ import 'place.dart';
import 'stub_data.dart';
class PlaceDetails extends StatefulWidget {
final Place place;
final ValueChanged<Place> onChanged;
const PlaceDetails({
@required this.place,
@required this.onChanged,
@ -15,9 +18,6 @@ class PlaceDetails extends StatefulWidget {
assert(onChanged != null),
super(key: key);
final Place place;
final ValueChanged<Place> onChanged;
@override
PlaceDetailsState createState() => PlaceDetailsState();
}
@ -26,10 +26,37 @@ class PlaceDetailsState extends State<PlaceDetails> {
Place _place;
GoogleMapController _mapController;
final Set<Marker> _markers = {};
final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${_place.name}'),
backgroundColor: Colors.green[700],
actions: [
Padding(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
child: IconButton(
icon: const Icon(Icons.save, size: 30.0),
onPressed: () {
widget.onChanged(_place);
Navigator.pop(context);
},
),
),
],
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: _detailsBody(),
),
);
}
@override
void initState() {
_place = widget.place;
@ -38,20 +65,10 @@ class PlaceDetailsState extends State<PlaceDetails> {
return super.initState();
}
void _onMapCreated(GoogleMapController controller) {
_mapController = controller;
setState(() {
_markers.add(Marker(
markerId: MarkerId(_place.latLng.toString()),
position: _place.latLng,
));
});
}
Widget _detailsBody() {
return ListView(
padding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 12.0),
children: <Widget>[
children: [
_NameTextField(
controller: _nameController,
onChanged: (value) {
@ -87,68 +104,21 @@ class PlaceDetailsState extends State<PlaceDetails> {
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${_place.name}'),
backgroundColor: Colors.green[700],
actions: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
child: IconButton(
icon: const Icon(Icons.save, size: 30.0),
onPressed: () {
widget.onChanged(_place);
Navigator.pop(context);
},
),
),
],
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: _detailsBody(),
),
);
void _onMapCreated(GoogleMapController controller) {
_mapController = controller;
setState(() {
_markers.add(Marker(
markerId: MarkerId(_place.latLng.toString()),
position: _place.latLng,
));
});
}
}
class _NameTextField extends StatelessWidget {
const _NameTextField({
@required this.controller,
@required this.onChanged,
Key key,
}) : assert(controller != null),
assert(onChanged != null),
super(key: key);
class _DescriptionTextField extends StatelessWidget {
final TextEditingController controller;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 16.0),
child: TextField(
decoration: const InputDecoration(
labelText: 'Name',
labelStyle: TextStyle(fontSize: 18.0),
),
style: const TextStyle(fontSize: 20.0, color: Colors.black87),
autocorrect: true,
controller: controller,
onChanged: (value) {
onChanged(value);
},
),
);
}
}
class _DescriptionTextField extends StatelessWidget {
final ValueChanged<String> onChanged;
const _DescriptionTextField({
@required this.controller,
@required this.onChanged,
@ -157,9 +127,6 @@ class _DescriptionTextField extends StatelessWidget {
assert(onChanged != null),
super(key: key);
final TextEditingController controller;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return Padding(
@ -181,38 +148,12 @@ class _DescriptionTextField extends StatelessWidget {
}
}
class _StarBar extends StatelessWidget {
const _StarBar({
@required this.rating,
@required this.onChanged,
Key key,
}) : assert(rating != null && rating >= 0 && rating <= maxStars),
assert(onChanged != null),
super(key: key);
static const int maxStars = 5;
final int rating;
final ValueChanged<int> onChanged;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(maxStars, (index) {
return IconButton(
icon: const Icon(Icons.star),
iconSize: 40.0,
color: rating > index ? Colors.amber : Colors.grey[400],
onPressed: () {
onChanged(index + 1);
},
);
}).toList(),
);
}
}
class _Map extends StatelessWidget {
final LatLng center;
final GoogleMapController mapController;
final ArgumentCallback<GoogleMapController> onMapCreated;
final Set<Marker> markers;
const _Map({
@required this.center,
@required this.mapController,
@ -223,24 +164,19 @@ class _Map extends StatelessWidget {
assert(onMapCreated != null),
super(key: key);
final LatLng center;
final GoogleMapController mapController;
final ArgumentCallback<GoogleMapController> onMapCreated;
final Set<Marker> markers;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 16.0),
elevation: 4.0,
elevation: 4,
child: SizedBox(
width: 340.0,
height: 240.0,
width: 340,
height: 240,
child: GoogleMap(
onMapCreated: onMapCreated,
initialCameraPosition: CameraPosition(
target: center,
zoom: 16.0,
zoom: 16,
),
markers: markers,
zoomGesturesEnabled: false,
@ -253,35 +189,95 @@ class _Map extends StatelessWidget {
}
}
class _NameTextField extends StatelessWidget {
final TextEditingController controller;
final ValueChanged<String> onChanged;
const _NameTextField({
@required this.controller,
@required this.onChanged,
Key key,
}) : assert(controller != null),
assert(onChanged != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 16),
child: TextField(
decoration: const InputDecoration(
labelText: 'Name',
labelStyle: TextStyle(fontSize: 18),
),
style: const TextStyle(fontSize: 20, color: Colors.black87),
autocorrect: true,
controller: controller,
onChanged: (value) {
onChanged(value);
},
),
);
}
}
class _Reviews extends StatelessWidget {
const _Reviews({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(
padding: EdgeInsets.fromLTRB(0, 12, 0, 8),
child: Align(
alignment: Alignment.topLeft,
child: Text(
'Reviews',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
color: Colors.black87,
),
),
),
),
Column(
children: StubData.reviewStrings
.map((reviewText) => _buildSingleReview(reviewText))
.toList(),
),
],
);
}
Widget _buildSingleReview(String reviewText) {
return Column(
children: <Widget>[
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
children: <Widget>[
children: [
Container(
width: 80.0,
height: 80.0,
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40.0),
borderRadius: BorderRadius.circular(40),
border: Border.all(
width: 3.0,
width: 3,
color: Colors.grey,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
children: const [
Text(
'5',
style: TextStyle(
fontSize: 24.0,
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
@ -289,16 +285,16 @@ class _Reviews extends StatelessWidget {
Icon(
Icons.star,
color: Colors.amber,
size: 36.0,
size: 36,
),
],
),
),
const SizedBox(width: 16.0),
const SizedBox(width: 16),
Expanded(
child: Text(
reviewText,
style: const TextStyle(fontSize: 20.0, color: Colors.black87),
style: const TextStyle(fontSize: 20, color: Colors.black87),
maxLines: null,
),
),
@ -306,38 +302,41 @@ class _Reviews extends StatelessWidget {
),
),
Divider(
height: 8.0,
height: 8,
color: Colors.grey[700],
),
],
);
}
}
class _StarBar extends StatelessWidget {
static const int maxStars = 5;
final int rating;
final ValueChanged<int> onChanged;
const _StarBar({
@required this.rating,
@required this.onChanged,
Key key,
}) : assert(rating != null && rating >= 0 && rating <= maxStars),
assert(onChanged != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 8.0),
child: Align(
alignment: Alignment.topLeft,
child: Text(
'Reviews',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
color: Colors.black87,
),
),
),
),
Column(
children: StubData.reviewStrings
.map((reviewText) => _buildSingleReview(reviewText))
.toList(),
),
],
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(maxStars, (index) {
return IconButton(
icon: const Icon(Icons.star),
iconSize: 40,
color: rating > index ? Colors.amber : Colors.grey[400],
onPressed: () {
onChanged(index + 1);
},
);
}).toList(),
);
}
}

@ -15,27 +15,11 @@ class PlaceList extends StatefulWidget {
class PlaceListState extends State<PlaceList> {
final ScrollController _scrollController = ScrollController();
void _onCategoryChanged(PlaceCategory newCategory) {
_scrollController.jumpTo(0.0);
Provider.of<AppState>(context, listen: false)
.setSelectedCategory(newCategory);
}
void _onPlaceChanged(Place value) {
// Replace the place with the modified version.
final newPlaces =
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
final index = newPlaces.indexWhere((place) => place.id == value.id);
newPlaces[index] = value;
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
@override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(context);
return Column(
children: <Widget>[
children: [
_ListCategoryButtonBar(
selectedCategory: state.selectedCategory,
onCategoryChanged: (value) => _onCategoryChanged(value),
@ -57,69 +41,72 @@ class PlaceListState extends State<PlaceList> {
],
);
}
void _onCategoryChanged(PlaceCategory newCategory) {
_scrollController.jumpTo(0.0);
Provider.of<AppState>(context, listen: false)
.setSelectedCategory(newCategory);
}
class _PlaceListTile extends StatelessWidget {
const _PlaceListTile({
void _onPlaceChanged(Place value) {
// Replace the place with the modified version.
final newPlaces =
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
final index = newPlaces.indexWhere((place) => place.id == value.id);
newPlaces[index] = value;
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
}
class _CategoryButton extends StatelessWidget {
final PlaceCategory category;
final bool selected;
final ValueChanged<PlaceCategory> onCategoryChanged;
const _CategoryButton({
Key key,
@required this.place,
@required this.onPlaceChanged,
}) : assert(place != null),
assert(onPlaceChanged != null),
@required this.category,
@required this.selected,
@required this.onCategoryChanged,
}) : assert(category != null),
assert(selected != null),
super(key: key);
final Place place;
final ValueChanged<Place> onPlaceChanged;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (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,
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,
),
maxLines: 3,
),
Row(
children: List.generate(5, (index) {
return Icon(
Icons.star,
size: 28.0,
color: place.starRating > index
? Colors.amber
: Colors.grey[400],
);
}).toList(),
),
Text(
place.description ?? '',
style: Theme.of(context).textTheme.subtitle1,
maxLines: 4,
overflow: TextOverflow.ellipsis,
child: ButtonTheme(
height: 50.0,
child: FlatButton(
child: Text(
_buttonText,
style: TextStyle(
fontSize: selected ? 20.0 : 18.0,
color: selected ? Colors.blue : Colors.black87,
),
SizedBox(height: 16.0),
Divider(
height: 2.0,
color: Colors.grey[700],
),
],
onPressed: () => onCategoryChanged(category),
),
),
);
@ -127,6 +114,9 @@ class _PlaceListTile extends StatelessWidget {
}
class _ListCategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedCategory;
final ValueChanged<PlaceCategory> onCategoryChanged;
const _ListCategoryButtonBar({
Key key,
@required this.selectedCategory,
@ -135,14 +125,11 @@ class _ListCategoryButtonBar extends StatelessWidget {
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>[
children: [
_CategoryButton(
category: PlaceCategory.favorite,
selected: selectedCategory == PlaceCategory.favorite,
@ -163,54 +150,67 @@ class _ListCategoryButtonBar extends StatelessWidget {
}
}
class _CategoryButton extends StatelessWidget {
const _CategoryButton({
class _PlaceListTile extends StatelessWidget {
final Place place;
final ValueChanged<Place> onPlaceChanged;
const _PlaceListTile({
Key key,
@required this.category,
@required this.selected,
@required this.onCategoryChanged,
}) : assert(category != null),
assert(selected != null),
@required this.place,
@required this.onPlaceChanged,
}) : assert(place != null),
assert(onPlaceChanged != 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,
return InkWell(
onTap: () => Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (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,
),
child: ButtonTheme(
height: 50.0,
child: FlatButton(
child: Text(
_buttonText,
style: TextStyle(
fontSize: selected ? 20.0 : 18.0,
color: selected ? Colors.blue : Colors.black87,
Row(
children: List.generate(5, (index) {
return Icon(
Icons.star,
size: 28.0,
color: place.starRating > index
? Colors.amber
: Colors.grey[400],
);
}).toList(),
),
Text(
place.description ?? '',
style: Theme.of(context).textTheme.subtitle1,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
onPressed: () => onCategoryChanged(category),
SizedBox(height: 16.0),
Divider(
height: 2.0,
color: Colors.grey[700],
),
],
),
),
);

@ -11,41 +11,55 @@ import 'place.dart';
import 'place_details.dart';
import 'place_tracker_app.dart';
class MapConfiguration {
final List<Place> places;
final PlaceCategory selectedCategory;
const MapConfiguration({
@required this.places,
@required this.selectedCategory,
}) : assert(places != null),
assert(selectedCategory != null);
@override
int get hashCode => places.hashCode ^ selectedCategory.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MapConfiguration &&
other.places == places &&
other.selectedCategory == selectedCategory;
}
static MapConfiguration of(AppState appState) {
return MapConfiguration(
places: appState.places,
selectedCategory: appState.selectedCategory,
);
}
}
class PlaceMap extends StatefulWidget {
final LatLng center;
const PlaceMap({
Key key,
this.center,
}) : super(key: key);
final LatLng center;
@override
PlaceMapState createState() => PlaceMapState();
}
class PlaceMapState extends State<PlaceMap> {
static Future<BitmapDescriptor> _getPlaceMarkerIcon(
BuildContext context, PlaceCategory category) async {
switch (category) {
case PlaceCategory.favorite:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/heart.png');
break;
case PlaceCategory.visited:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/visited.png');
break;
case PlaceCategory.wantToGo:
default:
return BitmapDescriptor.defaultMarker;
}
}
static List<Place> _getPlacesForCategory(
PlaceCategory category, List<Place> places) {
return places.where((place) => place.category == category).toList();
}
Completer<GoogleMapController> mapController = Completer();
MapType _currentMapType = MapType.normal;
@ -60,6 +74,50 @@ class PlaceMapState extends State<PlaceMap> {
MapConfiguration _configuration;
@override
Widget build(BuildContext context) {
_maybeUpdateMapConfiguration();
var state = Provider.of<AppState>(context);
return Builder(builder: (context) {
// We need this additional builder here so that we can pass its context to
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a
// SnackBar and to do this, we need a build context that has Scaffold as
// an ancestor.
return Center(
child: Stack(
children: [
GoogleMap(
onMapCreated: onMapCreated,
initialCameraPosition: CameraPosition(
target: widget.center,
zoom: 11.0,
),
mapType: _currentMapType,
markers: _markers,
onCameraMove: (position) => _lastMapPosition = position.target,
),
_CategoryButtonBar(
selectedPlaceCategory: state.selectedCategory,
visible: _pendingMarker == null,
onChanged: _switchSelectedCategory,
),
_AddPlaceButtonBar(
visible: _pendingMarker != null,
onSavePressed: () => _confirmAddPlace(context),
onCancelPressed: _cancelAddPlace,
),
_MapFabs(
visible: _pendingMarker == null,
onAddPlacePressed: _onAddPlacePressed,
onToggleMapTypePressed: _onToggleMapTypePressed,
),
],
),
);
});
}
Future<void> onMapCreated(GoogleMapController controller) async {
mapController.complete(controller);
_lastMapPosition = widget.center;
@ -83,6 +141,82 @@ class PlaceMapState extends State<PlaceMap> {
);
}
void _cancelAddPlace() {
if (_pendingMarker != null) {
setState(() {
_markers.remove(_pendingMarker);
_pendingMarker = null;
});
}
}
Future<void> _confirmAddPlace(BuildContext context) async {
if (_pendingMarker != null) {
// Create a new Place and map it to the marker we just added.
final newPlace = Place(
id: Uuid().v1(),
latLng: _pendingMarker.position,
name: _pendingMarker.infoWindow.title,
category:
Provider.of<AppState>(context, listen: false).selectedCategory,
);
var placeMarker = await _getPlaceMarkerIcon(context,
Provider.of<AppState>(context, listen: false).selectedCategory);
setState(() {
final updatedMarker = _pendingMarker.copyWith(
iconParam: placeMarker,
infoWindowParam: InfoWindow(
title: 'New Place',
snippet: null,
onTap: () => _pushPlaceDetailsScreen(newPlace),
),
draggableParam: false,
);
_updateMarker(
marker: _pendingMarker,
updatedMarker: updatedMarker,
place: newPlace,
);
_pendingMarker = null;
});
// Show a confirmation snackbar that has an action to edit the new place.
Scaffold.of(context).showSnackBar(
SnackBar(
duration: Duration(seconds: 3),
content:
const Text('New place added.', style: TextStyle(fontSize: 16.0)),
action: SnackBarAction(
label: 'Edit',
onPressed: () async {
_pushPlaceDetailsScreen(newPlace);
},
),
),
);
// Add the new place to the places stored in appState.
final newPlaces =
List<Place>.from(Provider.of<AppState>(context, listen: false).places)
..add(newPlace);
// Manually update our map configuration here since our map is already
// updated with the new marker. Otherwise, the map would be reconfigured
// in the main build method due to a modified AppState.
_configuration = MapConfiguration(
places: newPlaces,
selectedCategory:
Provider.of<AppState>(context, listen: false).selectedCategory,
);
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
}
Future<Marker> _createPlaceMarker(BuildContext context, Place place) async {
final marker = Marker(
markerId: MarkerId(place.latLng.toString()),
@ -100,18 +234,53 @@ class PlaceMapState extends State<PlaceMap> {
return marker;
}
void _pushPlaceDetailsScreen(Place place) {
assert(place != null);
Future<void> _maybeUpdateMapConfiguration() async {
_configuration ??=
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
final newConfiguration =
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (value) => _onPlaceChanged(value),
// Since we manually update [_configuration] when place or selectedCategory
// changes come from the [place_map], we should only enter this if statement
// when returning to the [place_map] after changes have been made from
// [place_list].
if (_configuration != newConfiguration && mapController != null) {
if (_configuration.places == newConfiguration.places &&
_configuration.selectedCategory !=
newConfiguration.selectedCategory) {
// If the configuration change is only a category change, just update
// the marker visibilities.
await _showPlacesForSelectedCategory(newConfiguration.selectedCategory);
} else {
// At this point, we know the places have been updated from the list
// view. We need to reconfigure the map to respect the updates.
newConfiguration.places
.where((p) => !_configuration.places.contains(p))
.map((value) => _updateExistingPlaceMarker(place: value));
await _zoomToFitPlaces(
_getPlacesForCategory(
newConfiguration.selectedCategory,
newConfiguration.places,
),
);
}),
}
_configuration = newConfiguration;
}
}
Future<void> _onAddPlacePressed() async {
setState(() {
final newMarker = Marker(
markerId: MarkerId(_lastMapPosition.toString()),
position: _lastMapPosition,
infoWindow: InfoWindow(title: 'New Place'),
draggable: true,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
);
_markers.add(newMarker);
_pendingMarker = newMarker;
});
}
void _onPlaceChanged(Place value) {
@ -135,37 +304,27 @@ class PlaceMapState extends State<PlaceMap> {
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
void _updateExistingPlaceMarker({@required Place place}) {
var marker = _markedPlaces.keys
.singleWhere((value) => _markedPlaces[value].id == place.id);
void _onToggleMapTypePressed() {
final nextType =
MapType.values[(_currentMapType.index + 1) % MapType.values.length];
setState(() {
final updatedMarker = marker.copyWith(
infoWindowParam: InfoWindow(
title: place.name,
snippet:
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
),
);
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
_currentMapType = nextType;
});
}
void _updateMarker({
@required Marker marker,
@required Marker updatedMarker,
@required Place place,
}) {
_markers.remove(marker);
_markedPlaces.remove(marker);
_markers.add(updatedMarker);
_markedPlaces[updatedMarker] = place;
}
void _pushPlaceDetailsScreen(Place place) {
assert(place != null);
Future<void> _switchSelectedCategory(PlaceCategory category) async {
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
await _showPlacesForSelectedCategory(category);
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (value) => _onPlaceChanged(value),
);
}),
);
}
Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async {
@ -190,213 +349,142 @@ class PlaceMapState extends State<PlaceMap> {
));
}
Future<void> _zoomToFitPlaces(List<Place> places) async {
var controller = await mapController.future;
// Default min/max values to latitude and longitude of center.
var minLat = widget.center.latitude;
var maxLat = widget.center.latitude;
var minLong = widget.center.longitude;
var maxLong = widget.center.longitude;
for (var place in places) {
minLat = min(minLat, place.latitude);
maxLat = max(maxLat, place.latitude);
minLong = min(minLong, place.longitude);
maxLong = max(maxLong, place.longitude);
}
await controller.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLong),
northeast: LatLng(maxLat, maxLong),
),
48.0,
),
);
}
Future<void> _onAddPlacePressed() async {
setState(() {
final newMarker = Marker(
markerId: MarkerId(_lastMapPosition.toString()),
position: _lastMapPosition,
infoWindow: InfoWindow(title: 'New Place'),
draggable: true,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
);
_markers.add(newMarker);
_pendingMarker = newMarker;
});
Future<void> _switchSelectedCategory(PlaceCategory category) async {
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
await _showPlacesForSelectedCategory(category);
}
Future<void> _confirmAddPlace(BuildContext context) async {
if (_pendingMarker != null) {
// Create a new Place and map it to the marker we just added.
final newPlace = Place(
id: Uuid().v1(),
latLng: _pendingMarker.position,
name: _pendingMarker.infoWindow.title,
category:
Provider.of<AppState>(context, listen: false).selectedCategory,
);
var placeMarker = await _getPlaceMarkerIcon(context,
Provider.of<AppState>(context, listen: false).selectedCategory);
setState(() {
final updatedMarker = _pendingMarker.copyWith(
iconParam: placeMarker,
infoWindowParam: InfoWindow(
title: 'New Place',
snippet: null,
onTap: () => _pushPlaceDetailsScreen(newPlace),
),
draggableParam: false,
);
_updateMarker(
marker: _pendingMarker,
updatedMarker: updatedMarker,
place: newPlace,
);
_pendingMarker = null;
});
// Show a confirmation snackbar that has an action to edit the new place.
Scaffold.of(context).showSnackBar(
SnackBar(
duration: Duration(seconds: 3),
content:
const Text('New place added.', style: TextStyle(fontSize: 16.0)),
action: SnackBarAction(
label: 'Edit',
onPressed: () async {
_pushPlaceDetailsScreen(newPlace);
},
),
),
);
// Add the new place to the places stored in appState.
final newPlaces =
List<Place>.from(Provider.of<AppState>(context, listen: false).places)
..add(newPlace);
// Manually update our map configuration here since our map is already
// updated with the new marker. Otherwise, the map would be reconfigured
// in the main build method due to a modified AppState.
_configuration = MapConfiguration(
places: newPlaces,
selectedCategory:
Provider.of<AppState>(context, listen: false).selectedCategory,
);
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
}
}
void _updateExistingPlaceMarker({@required Place place}) {
var marker = _markedPlaces.keys
.singleWhere((value) => _markedPlaces[value].id == place.id);
void _cancelAddPlace() {
if (_pendingMarker != null) {
setState(() {
_markers.remove(_pendingMarker);
_pendingMarker = null;
final updatedMarker = marker.copyWith(
infoWindowParam: InfoWindow(
title: place.name,
snippet:
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
),
);
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
});
}
}
void _onToggleMapTypePressed() {
final nextType =
MapType.values[(_currentMapType.index + 1) % MapType.values.length];
void _updateMarker({
@required Marker marker,
@required Marker updatedMarker,
@required Place place,
}) {
_markers.remove(marker);
_markedPlaces.remove(marker);
setState(() {
_currentMapType = nextType;
});
_markers.add(updatedMarker);
_markedPlaces[updatedMarker] = place;
}
Future<void> _maybeUpdateMapConfiguration() async {
_configuration ??=
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
final newConfiguration =
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
Future<void> _zoomToFitPlaces(List<Place> places) async {
var controller = await mapController.future;
// Since we manually update [_configuration] when place or selectedCategory
// changes come from the [place_map], we should only enter this if statement
// when returning to the [place_map] after changes have been made from
// [place_list].
if (_configuration != newConfiguration && mapController != null) {
if (_configuration.places == newConfiguration.places &&
_configuration.selectedCategory !=
newConfiguration.selectedCategory) {
// If the configuration change is only a category change, just update
// the marker visibilities.
await _showPlacesForSelectedCategory(newConfiguration.selectedCategory);
} else {
// At this point, we know the places have been updated from the list
// view. We need to reconfigure the map to respect the updates.
newConfiguration.places
.where((p) => !_configuration.places.contains(p))
.map((value) => _updateExistingPlaceMarker(place: value));
// Default min/max values to latitude and longitude of center.
var minLat = widget.center.latitude;
var maxLat = widget.center.latitude;
var minLong = widget.center.longitude;
var maxLong = widget.center.longitude;
await _zoomToFitPlaces(
_getPlacesForCategory(
newConfiguration.selectedCategory,
newConfiguration.places,
for (var place in places) {
minLat = min(minLat, place.latitude);
maxLat = max(maxLat, place.latitude);
minLong = min(minLong, place.longitude);
maxLong = max(maxLong, place.longitude);
}
await controller.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLong),
northeast: LatLng(maxLat, maxLong),
),
48.0,
),
);
}
_configuration = newConfiguration;
static Future<BitmapDescriptor> _getPlaceMarkerIcon(
BuildContext context, PlaceCategory category) async {
switch (category) {
case PlaceCategory.favorite:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/heart.png');
break;
case PlaceCategory.visited:
return BitmapDescriptor.fromAssetImage(
createLocalImageConfiguration(context), 'assets/visited.png');
break;
case PlaceCategory.wantToGo:
default:
return BitmapDescriptor.defaultMarker;
}
}
static List<Place> _getPlacesForCategory(
PlaceCategory category, List<Place> places) {
return places.where((place) => place.category == category).toList();
}
}
class _AddPlaceButtonBar extends StatelessWidget {
final bool visible;
final VoidCallback onSavePressed;
final VoidCallback onCancelPressed;
const _AddPlaceButtonBar({
Key key,
@required this.visible,
@required this.onSavePressed,
@required this.onCancelPressed,
}) : assert(visible != null),
assert(onSavePressed != null),
assert(onCancelPressed != null),
super(key: key);
@override
Widget build(BuildContext context) {
_maybeUpdateMapConfiguration();
var state = Provider.of<AppState>(context);
return Builder(builder: (context) {
// We need this additional builder here so that we can pass its context to
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a
// SnackBar and to do this, we need a build context that has Scaffold as
// an ancestor.
return Center(
child: Stack(
children: <Widget>[
GoogleMap(
onMapCreated: onMapCreated,
initialCameraPosition: CameraPosition(
target: widget.center,
zoom: 11.0,
),
mapType: _currentMapType,
markers: _markers,
onCameraMove: (position) => _lastMapPosition = position.target,
return Visibility(
visible: visible,
child: Container(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
alignment: Alignment.bottomCenter,
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: [
RaisedButton(
color: Colors.blue,
child: const Text(
'Save',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
_CategoryButtonBar(
selectedPlaceCategory: state.selectedCategory,
visible: _pendingMarker == null,
onChanged: _switchSelectedCategory,
onPressed: onSavePressed,
),
_AddPlaceButtonBar(
visible: _pendingMarker != null,
onSavePressed: () => _confirmAddPlace(context),
onCancelPressed: _cancelAddPlace,
RaisedButton(
color: Colors.red,
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
_MapFabs(
visible: _pendingMarker == null,
onAddPlacePressed: _onAddPlacePressed,
onToggleMapTypePressed: _onToggleMapTypePressed,
onPressed: onCancelPressed,
),
],
),
),
);
});
}
}
class _CategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedPlaceCategory;
final bool visible;
final ValueChanged<PlaceCategory> onChanged;
const _CategoryButtonBar({
Key key,
@required this.selectedPlaceCategory,
@ -407,10 +495,6 @@ class _CategoryButtonBar extends StatelessWidget {
assert(onChanged != null),
super(key: key);
final PlaceCategory selectedPlaceCategory;
final bool visible;
final ValueChanged<PlaceCategory> onChanged;
@override
Widget build(BuildContext context) {
return Visibility(
@ -420,7 +504,7 @@ class _CategoryButtonBar extends StatelessWidget {
alignment: Alignment.bottomCenter,
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
children: [
RaisedButton(
color: selectedPlaceCategory == PlaceCategory.favorite
? Colors.green[700]
@ -458,55 +542,11 @@ class _CategoryButtonBar extends StatelessWidget {
}
}
class _AddPlaceButtonBar extends StatelessWidget {
const _AddPlaceButtonBar({
Key key,
@required this.visible,
@required this.onSavePressed,
@required this.onCancelPressed,
}) : assert(visible != null),
assert(onSavePressed != null),
assert(onCancelPressed != null),
super(key: key);
class _MapFabs extends StatelessWidget {
final bool visible;
final VoidCallback onSavePressed;
final VoidCallback onCancelPressed;
@override
Widget build(BuildContext context) {
return Visibility(
visible: visible,
child: Container(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
alignment: Alignment.bottomCenter,
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Colors.blue,
child: const Text(
'Save',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
onPressed: onSavePressed,
),
RaisedButton(
color: Colors.red,
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
onPressed: onCancelPressed,
),
],
),
),
);
}
}
final VoidCallback onAddPlacePressed;
final VoidCallback onToggleMapTypePressed;
class _MapFabs extends StatelessWidget {
const _MapFabs({
Key key,
@required this.visible,
@ -517,10 +557,6 @@ class _MapFabs extends StatelessWidget {
assert(onToggleMapTypePressed != null),
super(key: key);
final bool visible;
final VoidCallback onAddPlacePressed;
final VoidCallback onToggleMapTypePressed;
@override
Widget build(BuildContext context) {
return Container(
@ -529,7 +565,7 @@ class _MapFabs extends StatelessWidget {
child: Visibility(
visible: visible,
child: Column(
children: <Widget>[
children: [
FloatingActionButton(
heroTag: 'add_place_button',
onPressed: onAddPlacePressed,
@ -552,39 +588,3 @@ class _MapFabs extends StatelessWidget {
);
}
}
class MapConfiguration {
const MapConfiguration({
@required this.places,
@required this.selectedCategory,
}) : assert(places != null),
assert(selectedCategory != null);
final List<Place> places;
final PlaceCategory selectedCategory;
@override
int get hashCode => places.hashCode ^ selectedCategory.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MapConfiguration &&
other.places == places &&
other.selectedCategory == selectedCategory;
}
static MapConfiguration of(AppState appState) {
return MapConfiguration(
places: appState.places,
selectedCategory: appState.selectedCategory,
);
}
}

@ -31,7 +31,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
appBar: AppBar(
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[
children: const [
Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
child: Icon(Icons.pin_drop, size: 24.0),
@ -40,7 +40,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
],
),
backgroundColor: Colors.green[700],
actions: <Widget>[
actions: [
Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
child: IconButton(
@ -63,7 +63,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
),
body: IndexedStack(
index: state.viewType == PlaceTrackerViewType.map ? 0 : 1,
children: <Widget>[
children: [
PlaceMap(center: const LatLng(45.521563, -122.677433)),
PlaceList()
],
@ -102,7 +102,6 @@ class AppState extends ChangeNotifier {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is AppState &&
other.places == places &&
other.selectedCategory == selectedCategory &&

@ -141,7 +141,7 @@ class StubData {
),
];
static const List<String> reviewStrings = [
static const reviewStrings = <String>[
'My favorite place in Portland. The employees are wonderful and so is the food. I go here at least once a month!',
'Staff was very friendly. Great atmosphere and good music. Would reccommend.',
'Best. Place. In. Town. Period.'

@ -56,7 +56,7 @@ packages:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.5"
cupertino_icons:
dependency: "direct main"
description:
@ -82,7 +82,7 @@ packages:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.7"
version: "1.0.8"
flutter_test:
dependency: "direct dev"
description: flutter
@ -94,14 +94,14 @@ packages:
name: google_maps_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.27+1"
version: "0.5.30"
google_maps_flutter_platform_interface:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.4"
matcher:
dependency: transitive
description:
@ -150,7 +150,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.5+1"
version: "4.3.2"
sky_engine:
dependency: transitive
description: flutter
@ -218,7 +218,7 @@ packages:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.2.0"
vector_math:
dependency: transitive
description:
@ -228,4 +228,4 @@ packages:
version: "2.0.8"
sdks:
dart: ">=2.9.0-14.0.dev <3.0.0"
flutter: ">=1.12.13+hotfix.5 <2.0.0"
flutter: ">=1.16.3 <2.0.0"

Loading…
Cancel
Save