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 | | infinite_list | filiph | 5/13/20 |
| isolate_example | johnpryan | 11/21/19 | | isolate_example | johnpryan | 11/21/19 |
| jsonexample | redbrogdon | 1/3/20 | | jsonexample | redbrogdon | 1/3/20 |
| place_tracker | | | | place_tracker | johnpryan | 8/13/20 |
| platform_channels | | | | platform_channels | | |
| platform_design | johnpryan | 10/7/19 | | platform_design | johnpryan | 10/7/19 |
| platform_view_swift | redbrogdon | 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` ### `place_map.dart`
This page shows a full-screen GoogleMap widget with place markers. Provides examples of how This page shows a full-screen GoogleMap widget with place markers. Provides
to stack other widgets on top of a GoogleMap widget, how to add markers to a map, and how to make examples of how to stack other widgets on top of a GoogleMap widget, how to add
other flutter widgets interact with the GoogleMap widget. markers to a map, and how to make other flutter widgets interact with the
GoogleMap widget.
### `place_details.dart` ### `place_details.dart`
This page shows a detailed view of a single place. Provides examples of how to place a This page shows a detailed view of a single place. Provides examples of how to
GoogleMap widget inside of a ListView and how to disable certain touch gestures on the map. place a GoogleMap widget inside of a ListView and how to disable certain touch
gestures on the map.
## Getting Started ## 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/>. 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 ```xml
<manifest ... <manifest ...
@ -41,14 +44,17 @@ Specify your API key in the application manifest `android/app/src/main/AndroidMa
android:value="YOUR KEY HERE"/> 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 For help getting started with Flutter, view our online
[documentation](https://flutter.io/). [documentation](https://flutter.io/).
## Caveat ## 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 * Dart APIs for controlling and interacting with a GoogleMap view from Flutter
code are still being consolidated and expanded. The intention is to grow code are still being consolidated and expanded. The intention is to grow
current coverage into a complete offering. Issues and pull requests aimed to current coverage into a complete offering. Issues and pull requests aimed to

@ -10,81 +10,32 @@ project 'Runner', {
'Release' => :release, 'Release' => :release,
} }
def parse_KV_file(file, separator='=') def flutter_root
file_abs_path = File.expand_path(file) generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
if !File.exists? file_abs_path unless File.exist?(generated_xcode_build_settings_path)
return []; 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_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
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end end
generated_key_values raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do target 'Runner' do
use_frameworks! use_frameworks!
use_modular_headers! use_modular_headers!
# Flutter Pod flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
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')
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"
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)
end
end
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
# Plugin Pods
# 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 end
# 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
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
target.build_configurations.each do |config| flutter_additional_ios_build_settings(target)
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end end
end end

@ -9,11 +9,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 60896A287CEE44DA12929F37 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AC813E96EEED8049372B007 /* Pods_Runner.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 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 */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -26,8 +23,6 @@
dstPath = ""; dstPath = "";
dstSubfolderSpec = 10; dstSubfolderSpec = 10;
files = ( files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
); );
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -37,14 +32,16 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -57,20 +54,36 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 60896A287CEE44DA12929F37 /* Pods_Runner.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup 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 */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */,
@ -84,6 +97,8 @@
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
8B9122CDB85400D9D46FA60E /* Pods */,
62C8D3D83EF1F84916A2F93E /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -125,12 +140,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
B606037D737D7C74CC55B500 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
CAF79D024134EA0C14FDEDC4 /* [CP] Embed Pods Frameworks */,
2FBA60F1C5E1C14B292C896D /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -189,6 +207,24 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase 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 */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -201,7 +237,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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 */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@ -217,6 +253,46 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -253,7 +329,6 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = { 249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -330,7 +405,6 @@
}; };
97C147031CF9000F007C117D /* Debug */ = { 97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -386,7 +460,6 @@
}; };
97C147041CF9000F007C117D /* Release */ = { 97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;

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

@ -1,13 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
enum PlaceCategory {
favorite,
visited,
wantToGo,
}
class Place { class Place {
final String id;
final LatLng latLng;
final String name;
final PlaceCategory category;
final String description;
final int starRating;
const Place({ const Place({
@required this.id, @required this.id,
@required this.latLng, @required this.latLng,
@ -21,13 +22,6 @@ class Place {
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 String name;
final PlaceCategory category;
final String description;
final int starRating;
double get latitude => latLng.latitude; double get latitude => latLng.latitude;
double get longitude => latLng.longitude; 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'; import 'stub_data.dart';
class PlaceDetails extends StatefulWidget { class PlaceDetails extends StatefulWidget {
final Place place;
final ValueChanged<Place> onChanged;
const PlaceDetails({ const PlaceDetails({
@required this.place, @required this.place,
@required this.onChanged, @required this.onChanged,
@ -15,9 +18,6 @@ class PlaceDetails extends StatefulWidget {
assert(onChanged != null), assert(onChanged != null),
super(key: key); super(key: key);
final Place place;
final ValueChanged<Place> onChanged;
@override @override
PlaceDetailsState createState() => PlaceDetailsState(); PlaceDetailsState createState() => PlaceDetailsState();
} }
@ -26,10 +26,37 @@ class PlaceDetailsState extends State<PlaceDetails> {
Place _place; Place _place;
GoogleMapController _mapController; GoogleMapController _mapController;
final Set<Marker> _markers = {}; final Set<Marker> _markers = {};
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = 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 @override
void initState() { void initState() {
_place = widget.place; _place = widget.place;
@ -38,20 +65,10 @@ class PlaceDetailsState extends State<PlaceDetails> {
return super.initState(); return super.initState();
} }
void _onMapCreated(GoogleMapController controller) {
_mapController = controller;
setState(() {
_markers.add(Marker(
markerId: MarkerId(_place.latLng.toString()),
position: _place.latLng,
));
});
}
Widget _detailsBody() { Widget _detailsBody() {
return ListView( return ListView(
padding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 12.0), padding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 12.0),
children: <Widget>[ children: [
_NameTextField( _NameTextField(
controller: _nameController, controller: _nameController,
onChanged: (value) { onChanged: (value) {
@ -87,68 +104,21 @@ class PlaceDetailsState extends State<PlaceDetails> {
); );
} }
@override void _onMapCreated(GoogleMapController controller) {
Widget build(BuildContext context) { _mapController = controller;
return Scaffold( setState(() {
appBar: AppBar( _markers.add(Marker(
title: Text('${_place.name}'), markerId: MarkerId(_place.latLng.toString()),
backgroundColor: Colors.green[700], position: _place.latLng,
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(),
),
);
} }
} }
class _NameTextField extends StatelessWidget { class _DescriptionTextField extends StatelessWidget {
const _NameTextField({
@required this.controller,
@required this.onChanged,
Key key,
}) : assert(controller != null),
assert(onChanged != null),
super(key: key);
final TextEditingController controller; final TextEditingController controller;
final ValueChanged<String> onChanged;
@override final ValueChanged<String> onChanged;
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 {
const _DescriptionTextField({ const _DescriptionTextField({
@required this.controller, @required this.controller,
@required this.onChanged, @required this.onChanged,
@ -157,9 +127,6 @@ class _DescriptionTextField extends StatelessWidget {
assert(onChanged != null), assert(onChanged != null),
super(key: key); super(key: key);
final TextEditingController controller;
final ValueChanged<String> onChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( 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 { class _Map extends StatelessWidget {
final LatLng center;
final GoogleMapController mapController;
final ArgumentCallback<GoogleMapController> onMapCreated;
final Set<Marker> markers;
const _Map({ const _Map({
@required this.center, @required this.center,
@required this.mapController, @required this.mapController,
@ -223,24 +164,19 @@ class _Map extends StatelessWidget {
assert(onMapCreated != null), assert(onMapCreated != null),
super(key: key); super(key: key);
final LatLng center;
final GoogleMapController mapController;
final ArgumentCallback<GoogleMapController> onMapCreated;
final Set<Marker> markers;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 16.0), margin: const EdgeInsets.symmetric(vertical: 16.0),
elevation: 4.0, elevation: 4,
child: SizedBox( child: SizedBox(
width: 340.0, width: 340,
height: 240.0, height: 240,
child: GoogleMap( child: GoogleMap(
onMapCreated: onMapCreated, onMapCreated: onMapCreated,
initialCameraPosition: CameraPosition( initialCameraPosition: CameraPosition(
target: center, target: center,
zoom: 16.0, zoom: 16,
), ),
markers: markers, markers: markers,
zoomGesturesEnabled: false, 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 { class _Reviews extends StatelessWidget {
const _Reviews({ const _Reviews({
Key key, Key key,
}) : super(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) { Widget _buildSingleReview(String reviewText) {
return Column( return Column(
children: <Widget>[ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0), padding: const EdgeInsets.symmetric(vertical: 10),
child: Row( child: Row(
children: <Widget>[ children: [
Container( Container(
width: 80.0, width: 80,
height: 80.0, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40.0), borderRadius: BorderRadius.circular(40),
border: Border.all( border: Border.all(
width: 3.0, width: 3,
color: Colors.grey, color: Colors.grey,
), ),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[ children: const [
Text( Text(
'5', '5',
style: TextStyle( style: TextStyle(
fontSize: 24.0, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87, color: Colors.black87,
), ),
@ -289,16 +285,16 @@ class _Reviews extends StatelessWidget {
Icon( Icon(
Icons.star, Icons.star,
color: Colors.amber, color: Colors.amber,
size: 36.0, size: 36,
), ),
], ],
), ),
), ),
const SizedBox(width: 16.0), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
reviewText, reviewText,
style: const TextStyle(fontSize: 20.0, color: Colors.black87), style: const TextStyle(fontSize: 20, color: Colors.black87),
maxLines: null, maxLines: null,
), ),
), ),
@ -306,38 +302,41 @@ class _Reviews extends StatelessWidget {
), ),
), ),
Divider( Divider(
height: 8.0, height: 8,
color: Colors.grey[700], 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Row(
children: <Widget>[ mainAxisAlignment: MainAxisAlignment.center,
const Padding( children: List.generate(maxStars, (index) {
padding: EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 8.0), return IconButton(
child: Align( icon: const Icon(Icons.star),
alignment: Alignment.topLeft, iconSize: 40,
child: Text( color: rating > index ? Colors.amber : Colors.grey[400],
'Reviews', onPressed: () {
style: TextStyle( onChanged(index + 1);
fontSize: 24.0, },
fontWeight: FontWeight.bold, );
decoration: TextDecoration.underline, }).toList(),
color: Colors.black87,
),
),
),
),
Column(
children: StubData.reviewStrings
.map((reviewText) => _buildSingleReview(reviewText))
.toList(),
),
],
); );
} }
} }

@ -15,27 +15,11 @@ class PlaceList extends StatefulWidget {
class PlaceListState extends State<PlaceList> { class PlaceListState extends State<PlaceList> {
final ScrollController _scrollController = ScrollController(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var state = Provider.of<AppState>(context); var state = Provider.of<AppState>(context);
return Column( return Column(
children: <Widget>[ children: [
_ListCategoryButtonBar( _ListCategoryButtonBar(
selectedCategory: state.selectedCategory, selectedCategory: state.selectedCategory,
onCategoryChanged: (value) => _onCategoryChanged(value), 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);
}
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 _PlaceListTile extends StatelessWidget { class _CategoryButton extends StatelessWidget {
const _PlaceListTile({ final PlaceCategory category;
final bool selected;
final ValueChanged<PlaceCategory> onCategoryChanged;
const _CategoryButton({
Key key, Key key,
@required this.place, @required this.category,
@required this.onPlaceChanged, @required this.selected,
}) : assert(place != null), @required this.onCategoryChanged,
assert(onPlaceChanged != null), }) : assert(category != null),
assert(selected != null),
super(key: key); super(key: key);
final Place place;
final ValueChanged<Place> onPlaceChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( String _buttonText;
onTap: () => Navigator.push<void>( switch (category) {
context, case PlaceCategory.favorite:
MaterialPageRoute(builder: (context) { _buttonText = 'Favorites';
return PlaceDetails( break;
place: place, case PlaceCategory.visited:
onChanged: (value) => onPlaceChanged(value), _buttonText = 'Visited';
); break;
}), case PlaceCategory.wantToGo:
), _buttonText = 'Want To Go';
child: Container( }
padding: EdgeInsets.only(top: 16.0),
child: Column( return Container(
crossAxisAlignment: CrossAxisAlignment.start, margin: EdgeInsets.symmetric(vertical: 12.0),
children: <Widget>[ decoration: BoxDecoration(
Text( border: Border(
place.name, bottom: BorderSide(
textAlign: TextAlign.left, color: selected ? Colors.blue : Colors.transparent,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
), ),
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( child: ButtonTheme(
place.description ?? '', height: 50.0,
style: Theme.of(context).textTheme.subtitle1, child: FlatButton(
maxLines: 4, child: Text(
overflow: TextOverflow.ellipsis, _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 { class _ListCategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedCategory;
final ValueChanged<PlaceCategory> onCategoryChanged;
const _ListCategoryButtonBar({ const _ListCategoryButtonBar({
Key key, Key key,
@required this.selectedCategory, @required this.selectedCategory,
@ -135,14 +125,11 @@ class _ListCategoryButtonBar extends StatelessWidget {
assert(onCategoryChanged != null), assert(onCategoryChanged != null),
super(key: key); super(key: key);
final PlaceCategory selectedCategory;
final ValueChanged<PlaceCategory> onCategoryChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[ children: [
_CategoryButton( _CategoryButton(
category: PlaceCategory.favorite, category: PlaceCategory.favorite,
selected: selectedCategory == PlaceCategory.favorite, selected: selectedCategory == PlaceCategory.favorite,
@ -163,54 +150,67 @@ class _ListCategoryButtonBar extends StatelessWidget {
} }
} }
class _CategoryButton extends StatelessWidget { class _PlaceListTile extends StatelessWidget {
const _CategoryButton({ final Place place;
final ValueChanged<Place> onPlaceChanged;
const _PlaceListTile({
Key key, Key key,
@required this.category, @required this.place,
@required this.selected, @required this.onPlaceChanged,
@required this.onCategoryChanged, }) : assert(place != null),
}) : assert(category != null), assert(onPlaceChanged != null),
assert(selected != null),
super(key: key); super(key: key);
final PlaceCategory category;
final bool selected;
final ValueChanged<PlaceCategory> onCategoryChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String _buttonText; return InkWell(
switch (category) { onTap: () => Navigator.push<void>(
case PlaceCategory.favorite: context,
_buttonText = 'Favorites'; MaterialPageRoute(builder: (context) {
break; return PlaceDetails(
case PlaceCategory.visited: place: place,
_buttonText = 'Visited'; onChanged: (value) => onPlaceChanged(value),
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: 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( Row(
height: 50.0, children: List.generate(5, (index) {
child: FlatButton( return Icon(
child: Text( Icons.star,
_buttonText, size: 28.0,
style: TextStyle( color: place.starRating > index
fontSize: selected ? 20.0 : 18.0, ? Colors.amber
color: selected ? Colors.blue : Colors.black87, : 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_details.dart';
import 'place_tracker_app.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 { class PlaceMap extends StatefulWidget {
final LatLng center;
const PlaceMap({ const PlaceMap({
Key key, Key key,
this.center, this.center,
}) : super(key: key); }) : super(key: key);
final LatLng center;
@override @override
PlaceMapState createState() => PlaceMapState(); PlaceMapState createState() => PlaceMapState();
} }
class PlaceMapState extends State<PlaceMap> { 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(); Completer<GoogleMapController> mapController = Completer();
MapType _currentMapType = MapType.normal; MapType _currentMapType = MapType.normal;
@ -60,6 +74,50 @@ class PlaceMapState extends State<PlaceMap> {
MapConfiguration _configuration; 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 { Future<void> onMapCreated(GoogleMapController controller) async {
mapController.complete(controller); mapController.complete(controller);
_lastMapPosition = widget.center; _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 { Future<Marker> _createPlaceMarker(BuildContext context, Place place) async {
final marker = Marker( final marker = Marker(
markerId: MarkerId(place.latLng.toString()), markerId: MarkerId(place.latLng.toString()),
@ -100,18 +234,53 @@ class PlaceMapState extends State<PlaceMap> {
return marker; return marker;
} }
void _pushPlaceDetailsScreen(Place place) { Future<void> _maybeUpdateMapConfiguration() async {
assert(place != null); _configuration ??=
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
final newConfiguration =
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
Navigator.push<void>( // Since we manually update [_configuration] when place or selectedCategory
context, // changes come from the [place_map], we should only enter this if statement
MaterialPageRoute(builder: (context) { // when returning to the [place_map] after changes have been made from
return PlaceDetails( // [place_list].
place: place, if (_configuration != newConfiguration && mapController != null) {
onChanged: (value) => _onPlaceChanged(value), 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) { void _onPlaceChanged(Place value) {
@ -135,37 +304,27 @@ class PlaceMapState extends State<PlaceMap> {
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces); Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
} }
void _updateExistingPlaceMarker({@required Place place}) { void _onToggleMapTypePressed() {
var marker = _markedPlaces.keys final nextType =
.singleWhere((value) => _markedPlaces[value].id == place.id); MapType.values[(_currentMapType.index + 1) % MapType.values.length];
setState(() { setState(() {
final updatedMarker = marker.copyWith( _currentMapType = nextType;
infoWindowParam: InfoWindow(
title: place.name,
snippet:
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
),
);
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
}); });
} }
void _updateMarker({ void _pushPlaceDetailsScreen(Place place) {
@required Marker marker, assert(place != null);
@required Marker updatedMarker,
@required Place place,
}) {
_markers.remove(marker);
_markedPlaces.remove(marker);
_markers.add(updatedMarker);
_markedPlaces[updatedMarker] = place;
}
Future<void> _switchSelectedCategory(PlaceCategory category) async { Navigator.push<void>(
Provider.of<AppState>(context, listen: false).setSelectedCategory(category); context,
await _showPlacesForSelectedCategory(category); MaterialPageRoute(builder: (context) {
return PlaceDetails(
place: place,
onChanged: (value) => _onPlaceChanged(value),
);
}),
);
} }
Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async { Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async {
@ -190,213 +349,142 @@ class PlaceMapState extends State<PlaceMap> {
)); ));
} }
Future<void> _zoomToFitPlaces(List<Place> places) async { Future<void> _switchSelectedCategory(PlaceCategory category) async {
var controller = await mapController.future; Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
await _showPlacesForSelectedCategory(category);
// 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> _confirmAddPlace(BuildContext context) async { void _updateExistingPlaceMarker({@required Place place}) {
if (_pendingMarker != null) { var marker = _markedPlaces.keys
// Create a new Place and map it to the marker we just added. .singleWhere((value) => _markedPlaces[value].id == place.id);
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 _cancelAddPlace() {
if (_pendingMarker != null) {
setState(() { setState(() {
_markers.remove(_pendingMarker); final updatedMarker = marker.copyWith(
_pendingMarker = null; infoWindowParam: InfoWindow(
title: place.name,
snippet:
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
),
);
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
}); });
} }
}
void _onToggleMapTypePressed() { void _updateMarker({
final nextType = @required Marker marker,
MapType.values[(_currentMapType.index + 1) % MapType.values.length]; @required Marker updatedMarker,
@required Place place,
}) {
_markers.remove(marker);
_markedPlaces.remove(marker);
setState(() { _markers.add(updatedMarker);
_currentMapType = nextType; _markedPlaces[updatedMarker] = place;
});
} }
Future<void> _maybeUpdateMapConfiguration() async { Future<void> _zoomToFitPlaces(List<Place> places) async {
_configuration ??= var controller = await mapController.future;
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
final newConfiguration =
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
// Since we manually update [_configuration] when place or selectedCategory // Default min/max values to latitude and longitude of center.
// changes come from the [place_map], we should only enter this if statement var minLat = widget.center.latitude;
// when returning to the [place_map] after changes have been made from var maxLat = widget.center.latitude;
// [place_list]. var minLong = widget.center.longitude;
if (_configuration != newConfiguration && mapController != null) { var maxLong = widget.center.longitude;
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( for (var place in places) {
_getPlacesForCategory( minLat = min(minLat, place.latitude);
newConfiguration.selectedCategory, maxLat = max(maxLat, place.latitude);
newConfiguration.places, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_maybeUpdateMapConfiguration(); return Visibility(
var state = Provider.of<AppState>(context); visible: visible,
child: Container(
return Builder(builder: (context) { padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
// We need this additional builder here so that we can pass its context to alignment: Alignment.bottomCenter,
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a child: ButtonBar(
// SnackBar and to do this, we need a build context that has Scaffold as alignment: MainAxisAlignment.center,
// an ancestor. children: [
return Center( RaisedButton(
child: Stack( color: Colors.blue,
children: <Widget>[ child: const Text(
GoogleMap( 'Save',
onMapCreated: onMapCreated, style: TextStyle(color: Colors.white, fontSize: 16.0),
initialCameraPosition: CameraPosition(
target: widget.center,
zoom: 11.0,
),
mapType: _currentMapType,
markers: _markers,
onCameraMove: (position) => _lastMapPosition = position.target,
), ),
_CategoryButtonBar( onPressed: onSavePressed,
selectedPlaceCategory: state.selectedCategory,
visible: _pendingMarker == null,
onChanged: _switchSelectedCategory,
), ),
_AddPlaceButtonBar( RaisedButton(
visible: _pendingMarker != null, color: Colors.red,
onSavePressed: () => _confirmAddPlace(context), child: const Text(
onCancelPressed: _cancelAddPlace, 'Cancel',
style: TextStyle(color: Colors.white, fontSize: 16.0),
), ),
_MapFabs( onPressed: onCancelPressed,
visible: _pendingMarker == null,
onAddPlacePressed: _onAddPlacePressed,
onToggleMapTypePressed: _onToggleMapTypePressed,
), ),
], ],
), ),
),
); );
});
} }
} }
class _CategoryButtonBar extends StatelessWidget { class _CategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedPlaceCategory;
final bool visible;
final ValueChanged<PlaceCategory> onChanged;
const _CategoryButtonBar({ const _CategoryButtonBar({
Key key, Key key,
@required this.selectedPlaceCategory, @required this.selectedPlaceCategory,
@ -407,10 +495,6 @@ class _CategoryButtonBar extends StatelessWidget {
assert(onChanged != null), assert(onChanged != null),
super(key: key); super(key: key);
final PlaceCategory selectedPlaceCategory;
final bool visible;
final ValueChanged<PlaceCategory> onChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Visibility( return Visibility(
@ -420,7 +504,7 @@ class _CategoryButtonBar extends StatelessWidget {
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ButtonBar( child: ButtonBar(
alignment: MainAxisAlignment.center, alignment: MainAxisAlignment.center,
children: <Widget>[ children: [
RaisedButton( RaisedButton(
color: selectedPlaceCategory == PlaceCategory.favorite color: selectedPlaceCategory == PlaceCategory.favorite
? Colors.green[700] ? Colors.green[700]
@ -458,55 +542,11 @@ class _CategoryButtonBar extends StatelessWidget {
} }
} }
class _AddPlaceButtonBar extends StatelessWidget { class _MapFabs 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);
final bool visible; final bool visible;
final VoidCallback onSavePressed; final VoidCallback onAddPlacePressed;
final VoidCallback onCancelPressed; final VoidCallback onToggleMapTypePressed;
@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,
),
],
),
),
);
}
}
class _MapFabs extends StatelessWidget {
const _MapFabs({ const _MapFabs({
Key key, Key key,
@required this.visible, @required this.visible,
@ -517,10 +557,6 @@ class _MapFabs extends StatelessWidget {
assert(onToggleMapTypePressed != null), assert(onToggleMapTypePressed != null),
super(key: key); super(key: key);
final bool visible;
final VoidCallback onAddPlacePressed;
final VoidCallback onToggleMapTypePressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -529,7 +565,7 @@ class _MapFabs extends StatelessWidget {
child: Visibility( child: Visibility(
visible: visible, visible: visible,
child: Column( child: Column(
children: <Widget>[ children: [
FloatingActionButton( FloatingActionButton(
heroTag: 'add_place_button', heroTag: 'add_place_button',
onPressed: onAddPlacePressed, 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( appBar: AppBar(
title: Row( title: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[ children: const [
Padding( Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
child: Icon(Icons.pin_drop, size: 24.0), child: Icon(Icons.pin_drop, size: 24.0),
@ -40,7 +40,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
], ],
), ),
backgroundColor: Colors.green[700], backgroundColor: Colors.green[700],
actions: <Widget>[ actions: [
Padding( Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0), padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
child: IconButton( child: IconButton(
@ -63,7 +63,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
), ),
body: IndexedStack( body: IndexedStack(
index: state.viewType == PlaceTrackerViewType.map ? 0 : 1, index: state.viewType == PlaceTrackerViewType.map ? 0 : 1,
children: <Widget>[ children: [
PlaceMap(center: const LatLng(45.521563, -122.677433)), PlaceMap(center: const LatLng(45.521563, -122.677433)),
PlaceList() PlaceList()
], ],
@ -102,7 +102,6 @@ class AppState extends ChangeNotifier {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is AppState && return other is AppState &&
other.places == places && other.places == places &&
other.selectedCategory == selectedCategory && 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!', '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.', 'Staff was very friendly. Great atmosphere and good music. Would reccommend.',
'Best. Place. In. Town. Period.' 'Best. Place. In. Town. Period.'

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