diff --git a/example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt b/example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt new file mode 100644 index 000000000..2b3594a43 --- /dev/null +++ b/example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example_livelist + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example_livelist/ios/Flutter/Debug.xcconfig b/example_livelist/ios/Flutter/Debug.xcconfig index 592ceee85..e8efba114 100644 --- a/example_livelist/ios/Flutter/Debug.xcconfig +++ b/example_livelist/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example_livelist/ios/Flutter/Release.xcconfig b/example_livelist/ios/Flutter/Release.xcconfig index 592ceee85..399e9340e 100644 --- a/example_livelist/ios/Flutter/Release.xcconfig +++ b/example_livelist/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example_livelist/ios/Podfile b/example_livelist/ios/Podfile new file mode 100644 index 000000000..6697f0a53 --- /dev/null +++ b/example_livelist/ios/Podfile @@ -0,0 +1,87 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + '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') + 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 + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/example_livelist/ios/Podfile.lock b/example_livelist/ios/Podfile.lock new file mode 100644 index 000000000..9c84f5c0e --- /dev/null +++ b/example_livelist/ios/Podfile.lock @@ -0,0 +1,77 @@ +PODS: + - connectivity (0.0.1): + - Flutter + - Reachability + - connectivity_macos (0.0.1): + - Flutter + - devicelocale (0.0.1): + - Flutter + - Flutter (1.0.0) + - package_info (0.0.1): + - Flutter + - path_provider (0.0.1): + - Flutter + - path_provider_macos (0.0.1): + - Flutter + - Reachability (3.2) + - shared_preferences (0.0.1): + - Flutter + - shared_preferences_macos (0.0.1): + - Flutter + - shared_preferences_web (0.0.1): + - Flutter + +DEPENDENCIES: + - connectivity (from `.symlinks/plugins/connectivity/ios`) + - connectivity_macos (from `.symlinks/plugins/connectivity_macos/ios`) + - devicelocale (from `.symlinks/plugins/devicelocale/ios`) + - Flutter (from `Flutter`) + - package_info (from `.symlinks/plugins/package_info/ios`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) + - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) + - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) + - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) + +SPEC REPOS: + trunk: + - Reachability + +EXTERNAL SOURCES: + connectivity: + :path: ".symlinks/plugins/connectivity/ios" + connectivity_macos: + :path: ".symlinks/plugins/connectivity_macos/ios" + devicelocale: + :path: ".symlinks/plugins/devicelocale/ios" + Flutter: + :path: Flutter + package_info: + :path: ".symlinks/plugins/package_info/ios" + path_provider: + :path: ".symlinks/plugins/path_provider/ios" + path_provider_macos: + :path: ".symlinks/plugins/path_provider_macos/ios" + shared_preferences: + :path: ".symlinks/plugins/shared_preferences/ios" + shared_preferences_macos: + :path: ".symlinks/plugins/shared_preferences_macos/ios" + shared_preferences_web: + :path: ".symlinks/plugins/shared_preferences_web/ios" + +SPEC CHECKSUMS: + connectivity: 6e94255659cc86dcbef1d452ad3e0491bb1b3e75 + connectivity_macos: e2e9731b6b22dda39eb1b128f6969d574460e191 + devicelocale: feebbe5e7a30adb8c4f83185de1b50ff19b44f00 + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + package_info: 48b108e75b8802c2d5e126f208ef540561c98aef + path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d + path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 + shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 + shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 + shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 + +PODFILE CHECKSUM: c34e2287a9ccaa606aeceab922830efb9a6ff69a + +COCOAPODS: 1.9.1 diff --git a/example_livelist/ios/Runner.xcodeproj/project.pbxproj b/example_livelist/ios/Runner.xcodeproj/project.pbxproj index 553f2c54b..1bcee6d82 100644 --- a/example_livelist/ios/Runner.xcodeproj/project.pbxproj +++ b/example_livelist/ios/Runner.xcodeproj/project.pbxproj @@ -9,14 +9,11 @@ /* 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, ); }; }; 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 */; }; + AA9ED002A01557CA2011895D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE09BC6348AC3D01488A6AC5 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -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; @@ -38,18 +33,20 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 764A30AF2F41732B90E56144 /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 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 = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AFD86D678FFE3E0E17B5F751 /* 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 = ""; }; + E161505868E40C07D1C51D61 /* 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 = ""; }; + EE09BC6348AC3D01488A6AC5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,20 +54,28 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + AA9ED002A01557CA2011895D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1CA33D075B52907741216411 /* Pods */ = { + isa = PBXGroup; + children = ( + 764A30AF2F41732B90E56144 /* Pods-Runner.debug.xcconfig */, + AFD86D678FFE3E0E17B5F751 /* Pods-Runner.release.xcconfig */, + E161505868E40C07D1C51D61 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 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 +89,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 1CA33D075B52907741216411 /* Pods */, + B23A141FCB739252F1C84813 /* Frameworks */, ); sourceTree = ""; }; @@ -118,6 +125,14 @@ name = "Supporting Files"; sourceTree = ""; }; + B23A141FCB739252F1C84813 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EE09BC6348AC3D01488A6AC5 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -125,12 +140,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 55160CB97B8AD8ECD81974ED /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 7239A0A335A6A90C11214E43 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -201,7 +218,59 @@ ); 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"; + }; + 55160CB97B8AD8ECD81974ED /* [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; + }; + 7239A0A335A6A90C11214E43 /* [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", + "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/connectivity/connectivity.framework", + "${BUILT_PRODUCTS_DIR}/devicelocale/devicelocale.framework", + "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", + "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/devicelocale.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; diff --git a/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata b/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16e..21a3cc14c 100644 --- a/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example_livelist/lib/main.dart b/example_livelist/lib/main.dart index 1c07d3639..bc537c2fc 100644 --- a/example_livelist/lib/main.dart +++ b/example_livelist/lib/main.dart @@ -60,13 +60,10 @@ class _MyAppState extends State { } Future initData() async { - await Parse().initialize( - keyParseApplicationId, - keyParseServerUrl, - clientKey: keyParseClientKey, - debug: true, - liveQueryUrl: keyParseLiveServerUrl, - ); + await Parse().initialize(keyParseApplicationId, keyParseServerUrl, + clientKey: keyParseClientKey, + debug: keyDebug, + liveQueryUrl: keyParseLiveServerUrl); return (await Parse().healthCheck()).success; } diff --git a/example_livelist/web/favicon.png b/example_livelist/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/example_livelist/web/favicon.png differ diff --git a/example_livelist/web/icons/Icon-192.png b/example_livelist/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/example_livelist/web/icons/Icon-192.png differ diff --git a/example_livelist/web/icons/Icon-512.png b/example_livelist/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/example_livelist/web/icons/Icon-512.png differ diff --git a/example_livelist/web/index.html b/example_livelist/web/index.html new file mode 100644 index 000000000..15920bc8d --- /dev/null +++ b/example_livelist/web/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + example_livelist + + + + + + + + diff --git a/example_livelist/web/manifest.json b/example_livelist/web/manifest.json new file mode 100644 index 000000000..f71fe9f5e --- /dev/null +++ b/example_livelist/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example_livelist", + "short_name": "example_livelist", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/lib/src/network/parse_live_query_web.dart b/lib/src/network/parse_live_query_web.dart index 59a9ce6df..7851c3e74 100644 --- a/lib/src/network/parse_live_query_web.dart +++ b/lib/src/network/parse_live_query_web.dart @@ -1,20 +1,132 @@ +import 'dart:async'; import 'dart:convert'; -// ignore: uri_does_not_exist import 'dart:html' as html; +import 'package:connectivity/connectivity.dart'; +import 'package:flutter/widgets.dart'; + import '../../parse_server_sdk.dart'; -enum LiveQueryEvent { - create, - enter, - update, - leave, - delete, - error +enum LiveQueryEvent { create, enter, update, leave, delete, error } + +const String _printConstLiveQuery = 'LiveQuery: '; + +class Subscription { + Subscription(this.query, this.requestId, {T copyObject}) { + _copyObject = copyObject; + } + QueryBuilder query; + T _copyObject; + int requestId; + bool _enabled = false; + final List _liveQueryEvent = [ + 'create', + 'enter', + 'update', + 'leave', + 'delete', + 'error' + ]; + Map eventCallbacks = {}; + void on(LiveQueryEvent op, Function callback) { + eventCallbacks[_liveQueryEvent[op.index]] = callback; + } + + T get copyObject { + return _copyObject; + } } -class LiveQuery { - LiveQuery({bool debug, ParseHTTPClient client, bool autoSendSessionId}) { +enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } + +class LiveQueryReconnectingController with WidgetsBindingObserver { + // -1 means "do not try to reconnect", + static const List retryInterval = [0, 500, 1000, 2000, 5000]; + static const String DEBUG_TAG = 'LiveQueryReconnectingController'; + + final Function _reconnect; + final Stream _eventStream; + final bool debug; + + int _retryState = 0; + bool _isOnline = false; + bool _isConnected = false; + bool _userDisconnected = false; + + Timer _currentTimer; + + LiveQueryReconnectingController( + this._reconnect, this._eventStream, this.debug) { + _connectivityChanged(ConnectivityResult.wifi); + _eventStream.listen((LiveQueryClientEvent event) { + switch (event) { + case LiveQueryClientEvent.CONNECTED: + _isConnected = true; + _retryState = 0; + _userDisconnected = false; + break; + case LiveQueryClientEvent.DISCONNECTED: + _isConnected = false; + _setReconnect(); + break; + case LiveQueryClientEvent.USER_DISCONNECTED: + _userDisconnected = true; + if (_currentTimer != null) { + _currentTimer.cancel(); + _currentTimer = null; + } + break; + } + + if (debug) print('$DEBUG_TAG: $event'); + }); + WidgetsBinding.instance.addObserver(this); + } + + void _connectivityChanged(ConnectivityResult state) { + if (!_isOnline && state != ConnectivityResult.none) _retryState = 0; + _isOnline = state != ConnectivityResult.none; + if (debug) print('$DEBUG_TAG: $state'); + _setReconnect(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + _setReconnect(); + break; + default: + break; + } + } + + void _setReconnect() { + if (_isOnline && + !_isConnected && + _currentTimer == null && + !_userDisconnected && + retryInterval[_retryState] >= 0) { + _currentTimer = + Timer(Duration(milliseconds: retryInterval[_retryState]), () { + _currentTimer = null; + _reconnect(); + }); + if (debug) + print('$DEBUG_TAG: Retrytimer set to ${retryInterval[_retryState]}ms'); + if (_retryState < retryInterval.length - 1) _retryState++; + } + } +} + +class Client { + factory Client() => _getInstance(); + Client._internal( + {bool debug, ParseHTTPClient client, bool autoSendSessionId}) { + _clientEventStreamController = StreamController(); + _clientEventStream = + _clientEventStreamController.stream.asBroadcastStream(); + _client = client ?? ParseHTTPClient( sendSessionId: @@ -24,57 +136,132 @@ class LiveQuery { _debug = isDebugEnabled(objectLevelDebug: debug); _sendSessionId = autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; + _liveQueryURL = _client.data.liveQueryURL; + if (_liveQueryURL.contains('https')) { + _liveQueryURL = _liveQueryURL.replaceAll('https', 'wss'); + } else if (_liveQueryURL.contains('http')) { + _liveQueryURL = _liveQueryURL.replaceAll('http', 'ws'); + } + + reconnectingController = LiveQueryReconnectingController( + () => reconnect(userInitialized: false), getClientEventStream, _debug); + } + static Client get instance => _getInstance(); + static Client _instance; + static Client _getInstance( + {bool debug, ParseHTTPClient client, bool autoSendSessionId}) { + _instance ??= Client._internal( + debug: debug, client: client, autoSendSessionId: autoSendSessionId); + return _instance; + } + + Stream get getClientEventStream { + return _clientEventStream; } html.WebSocket _webSocket; ParseHTTPClient _client; bool _debug; bool _sendSessionId; - Map _connectMessage; - Map _subscribeMessage; - Map _unsubscribeMessage; - Map eventCallbacks = {}; - int _requestIdCount = 1; - final List _liveQueryEvent = [ - 'create', - 'enter', - 'update', - 'leave', - 'delete', - 'error' - ]; - final String _printConstLiveQuery = 'LiveQuery: '; + Stream _stream; + String _liveQueryURL; + bool _connecting = false; + StreamController _clientEventStreamController; + Stream _clientEventStream; + LiveQueryReconnectingController reconnectingController; - int _requestIdGenerator() { - return _requestIdCount++; + final Map _requestSubScription = {}; + + Future reconnect({bool userInitialized = false}) async { + await _connect(userInitialized: userInitialized); + _connectLiveQuery(); } - // ignore: always_specify_types - Future subscribe(QueryBuilder query) async { - String _liveQueryURL = _client.data.liveQueryURL; - if (_liveQueryURL.contains('https')) { - _liveQueryURL = _liveQueryURL.replaceAll('https', 'wss'); - } else if (_liveQueryURL.contains('http')) { - _liveQueryURL = _liveQueryURL.replaceAll('http', 'ws'); + int readyState() { + if (_webSocket != null) { + return _webSocket.readyState; } + return html.WebSocket.CONNECTING; + } - final String _className = query.object.parseClassName; - final List keysToReturn = query.limiters['keys']?.split(','); - query.limiters.clear(); //Remove limits in LiveQuery - final String _where = query.buildQuery().replaceAll('where=', ''); - - //Convert where condition to Map - Map _whereMap = Map(); - if (_where != '') { - _whereMap = json.decode(_where); + Future disconnect({bool userInitialized = false}) async { + if (_webSocket != null && _webSocket.readyState == html.WebSocket.OPEN) { + if (_debug) { + print('$_printConstLiveQuery: Socket closed'); + } + await _webSocket.close(); + _webSocket = null; + } + if (_webSocket != null) { + if (_debug) { + print('$_printConstLiveQuery: close'); + } + _webSocket.close(); + _webSocket = null; + _stream = null; } + _requestSubScription.values.toList().forEach((Subscription subcription) { + subcription._enabled = false; + }); + _connecting = false; + if (userInitialized) + _clientEventStreamController.sink + .add(LiveQueryClientEvent.USER_DISCONNECTED); + } + Future> subscribe( + QueryBuilder query, + {T copyObject}) async { + if (_webSocket == null) { + await _clientEventStream.any((LiveQueryClientEvent event) => + event == LiveQueryClientEvent.CONNECTED); + } final int requestId = _requestIdGenerator(); + final Subscription subscription = + Subscription(query, requestId, copyObject: copyObject); + _requestSubScription[requestId] = subscription; + //After a client connects to the LiveQuery server, + //it can send a subscribe message to subscribe a ParseQuery. + _subscribeLiveQuery(subscription); + return subscription; + } + + void unSubscribe(Subscription subscription) { + //Mount message for Unsubscribe + final Map unsubscribeMessage = { + 'op': 'unsubscribe', + 'requestId': subscription.requestId, + }; + if (_webSocket != null) { + if (_debug) { + print('$_printConstLiveQuery: UnsubscribeMessage: $unsubscribeMessage'); + } + _webSocket.send(jsonEncode(unsubscribeMessage)); +// _channel.sink.add(jsonEncode(unsubscribeMessage)); + subscription._enabled = false; + _requestSubScription.remove(subscription.requestId); + } + } + + static int _requestIdCount = 1; + + int _requestIdGenerator() { + return _requestIdCount++; + } + + Future _connect({bool userInitialized = false}) async { + if (_connecting) { + print('already connecting'); + return Future.value(null); + } + await disconnect(userInitialized: userInitialized); + _connecting = true; try { _webSocket = html.WebSocket(_liveQueryURL); await _webSocket.onOpen.first; + _connecting = false; if (_webSocket != null && _webSocket.readyState == html.WebSocket.OPEN) { if (_debug) { print('$_printConstLiveQuery: Socket opened'); @@ -82,107 +269,181 @@ class LiveQuery { } else { if (_debug) { print('$_printConstLiveQuery: Error when connection client'); - return Future.value(null); } + return Future.value(null); } + _stream = _webSocket.onMessage; - _webSocket.onMessage.listen((html.MessageEvent e) { - final dynamic message = e.data; - if (_debug) { - print('$_printConstLiveQuery: Listen: $message'); - } - - final Map actionData = jsonDecode(message); - - if (eventCallbacks.containsKey(actionData['op'])) { - if (actionData.containsKey('object')) { - final Map map = actionData['object']; - final String className = map['className']; - eventCallbacks[actionData['op']]( - ParseObject(className).fromJson(map)); - } else { - eventCallbacks[actionData['op']](actionData); - } - } + _stream.listen((html.MessageEvent event) { + final dynamic message = event.data; + _handleMessage(message); }, onDone: () { + _clientEventStreamController.sink + .add(LiveQueryClientEvent.DISCONNECTED); if (_debug) { print('$_printConstLiveQuery: Done'); } }, onError: (Object error) { + _clientEventStreamController.sink + .add(LiveQueryClientEvent.DISCONNECTED); if (_debug) { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); } return Future.value(handleException( - Exception(error), ParseApiRQ.liveQuery, _debug, _className)); + Exception(error), ParseApiRQ.liveQuery, _debug, 'HtmlWebSocket')); }); - - //The connect message is sent from a client to the LiveQuery server. - //It should be the first message sent from a client after the WebSocket connection is established. - _connectMessage = { - 'op': 'connect', - 'applicationId': _client.data.applicationId - }; - if (_sendSessionId) { - _connectMessage['sessionToken'] = _client.data.sessionId; + } on Exception catch (e) { + _connecting = false; + _clientEventStreamController.sink.add(LiveQueryClientEvent.DISCONNECTED); + if (_debug) { + print('$_printConstLiveQuery: Error: ${e.toString()}'); } + return handleException(e, ParseApiRQ.liveQuery, _debug, 'LiveQuery'); + } + } - if (_client.data.clientKey != null) - _connectMessage['clientKey'] = _client.data.clientKey; - if (_client.data.masterKey != null) - _connectMessage['masterKey'] = _client.data.masterKey; + void _connectLiveQuery() { + if (_webSocket == null) { + return; + } + //The connect message is sent from a client to the LiveQuery server. + //It should be the first message sent from a client after the WebSocket connection is established. + final Map connectMessage = { + 'op': 'connect', + 'applicationId': _client.data.applicationId + }; - if (_debug) { - print('$_printConstLiveQuery: ConnectMessage: $_connectMessage'); - } - _webSocket.sendString(jsonEncode(_connectMessage)); - - //After a client connects to the LiveQuery server, - //it can send a subscribe message to subscribe a ParseQuery. - _subscribeMessage = { - 'op': 'subscribe', - 'requestId': requestId, - 'query': { - 'className': _className, - 'where': _whereMap, - if (keysToReturn != null && keysToReturn.isNotEmpty) - 'fields': keysToReturn - } - }; - if (_sendSessionId) { - _subscribeMessage['sessionToken'] = _client.data.sessionId; - } + if (_sendSessionId && _client.data.sessionId != null) { + connectMessage['sessionToken'] = _client.data.sessionId; + } - if (_debug) { - print('$_printConstLiveQuery: SubscribeMessage: $_subscribeMessage'); + if (_client.data.clientKey != null) + connectMessage['clientKey'] = _client.data.clientKey; + if (_client.data.masterKey != null) + connectMessage['masterKey'] = _client.data.masterKey; + + if (_debug) { + print('$_printConstLiveQuery: ConnectMessage: $connectMessage'); + } + _webSocket.send(jsonEncode(connectMessage)); +// _channel.sink.add(jsonEncode(connectMessage)); + } + + void _subscribeLiveQuery(Subscription subscription) { + if (subscription._enabled) { + return; + } + subscription._enabled = true; + QueryBuilder query = subscription.query; + final List keysToReturn = query.limiters['keys']?.split(','); + query.limiters.clear(); //Remove limits in LiveQuery + final String _where = query.buildQuery().replaceAll('where=', ''); + + //Convert where condition to Map + Map _whereMap = Map(); + if (_where != '') { + _whereMap = json.decode(_where); + } + + final Map subscribeMessage = { + 'op': 'subscribe', + 'requestId': subscription.requestId, + 'query': { + 'className': query.object.parseClassName, + 'where': _whereMap, + if (keysToReturn != null && keysToReturn.isNotEmpty) + 'fields': keysToReturn } + }; + if (_sendSessionId && _client.data.sessionId != null) { + subscribeMessage['sessionToken'] = _client.data.sessionId; + } - _webSocket.sendString(jsonEncode(_subscribeMessage)); + if (_debug) { + print('$_printConstLiveQuery: SubscribeMessage: $subscribeMessage'); + } - //Mount message for Unsubscribe - _unsubscribeMessage = { - 'op': 'unsubscribe', - 'requestId': requestId, - }; - } on Exception catch (e) { - if (_debug) { - print('$_printConstLiveQuery: Error: ${e.toString()}'); + _webSocket.send(jsonEncode(subscribeMessage)); +// _channel.sink.add(jsonEncode(subscribeMessage)); + } + + void _handleMessage(String message) { + if (_debug) { + print('$_printConstLiveQuery: Listen: $message'); + } + + final Map actionData = jsonDecode(message); + Subscription subscription; + if (actionData.containsKey('op') && actionData['op'] == 'connected') { + print('ReSubScription:$_requestSubScription'); + _requestSubScription.values.toList().forEach((Subscription subcription) { + _subscribeLiveQuery(subcription); + }); + _clientEventStreamController.sink.add(LiveQueryClientEvent.CONNECTED); + return; + } + if (actionData.containsKey('requestId')) { + subscription = _requestSubScription[actionData['requestId']]; + } + if (subscription == null) { + return; + } + if (subscription.eventCallbacks.containsKey(actionData['op'])) { + if (actionData.containsKey('object')) { + final Map map = actionData['object']; + final String className = map['className']; + if (className == '_User') { + subscription.eventCallbacks[actionData['op']]( + (subscription.copyObject ?? ParseUser(null, null, null)) + .fromJson(map)); + } else { + subscription.eventCallbacks[actionData['op']]( + (subscription.copyObject ?? ParseObject(className)) + .fromJson(map)); + } + } else { + subscription.eventCallbacks[actionData['op']](actionData); } - return handleException(e, ParseApiRQ.liveQuery, _debug, _className); } } +} - void on(LiveQueryEvent op, Function callback) { - eventCallbacks[_liveQueryEvent[op.index]] = callback; +class LiveQuery { + LiveQuery({bool debug, ParseHTTPClient client, bool autoSendSessionId}) { + _client = client ?? + ParseHTTPClient( + sendSessionId: + autoSendSessionId ?? ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); + + _debug = isDebugEnabled(objectLevelDebug: debug); + _sendSessionId = + autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; + this.client = Client._getInstance( + client: _client, debug: _debug, autoSendSessionId: _sendSessionId); + } + + ParseHTTPClient _client; + bool _debug; + bool _sendSessionId; + Subscription _latestSubscription; + Client client; + + // ignore: always_specify_types + @deprecated + Future subscribe(QueryBuilder query) async { + _latestSubscription = await client.subscribe(query); + return _latestSubscription; } + @deprecated Future unSubscribe() async { - if (_webSocket != null && _webSocket.readyState == html.WebSocket.OPEN) { - _webSocket.sendString(jsonEncode(_unsubscribeMessage)); - if (_debug) { - print('$_printConstLiveQuery: Socket closed'); - } - await _webSocket.close(); - } + client.unSubscribe(_latestSubscription); + } + + @deprecated + void on(LiveQueryEvent op, Function callback) { + _latestSubscription.on(op, callback); } } diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index bf5b78068..308b1dea9 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -65,7 +65,7 @@ class ParseLiveList { } Stream> get stream => _eventStreamController.stream; - Subscription _liveQuerySubscription; + Subscription _liveQuerySubscription; StreamSubscription _liveQueryClientEventSubscription; Future _runQuery() async { @@ -89,9 +89,10 @@ class ParseLiveList { final ParseResponse parseResponse = await _runQuery(); if (parseResponse.success) { _list = parseResponse.results - ?.map>( - (dynamic element) => ParseLiveListElement(element)) - ?.toList() ?? List>(); + ?.map>( + (dynamic element) => ParseLiveListElement(element)) + ?.toList() ?? + List>(); } LiveQuery() @@ -130,8 +131,9 @@ class ParseLiveList { if (newList[j] .get(keyVarUpdatedAt) .isAfter(currentObject.get(keyVarUpdatedAt))) { - final QueryBuilder queryBuilder = QueryBuilder.copy(_query) - ..whereEqualTo(keyVarObjectId, currentObjectId); + final QueryBuilder queryBuilder = + QueryBuilder.copy(_query) + ..whereEqualTo(keyVarObjectId, currentObjectId); queryBuilder.query().then((ParseResponse result) { if (result.success && result.results != null) { _objectUpdated(result.results.first);