diff --git a/example/.vscode/launch.json b/example/.vscode/launch.json index 5086a1da4..f3d4353ca 100644 --- a/example/.vscode/launch.json +++ b/example/.vscode/launch.json @@ -1,6 +1,18 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Flutter", + "type": "dart", + "request": "launch", + "program": "lib/main.dart" + }, + { + "name": "Dart", + "type": "dart", + "request": "launch", + "program": "bin/main.dart" + }, { "name": "Flutter Desktop Attach", "request": "attach", @@ -8,4 +20,4 @@ "type": "dart" } ] -} \ No newline at end of file +} diff --git a/example/README.md b/example/README.md index 17ecae4d2..30977327b 100644 --- a/example/README.md +++ b/example/README.md @@ -1,8 +1,10 @@ -# flutter_plugin_example +# flutter_ParseServer_example + + +Demonstrates ParseServer sdk. +## 连接 docker server +[docker-compose-parse-server](https://github.com/unreal0/docker-compose-parse-server). -Demonstrates how to use the flutter_plugin plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.io/). +## demo +* live parse demo +* data add/delete/change/find diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 9883bcabb..062011fe4 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 27 + compileSdkVersion 28 lintOptions { disable 'InvalidPackage' @@ -25,7 +25,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.flutterpluginexample" minSdkVersion 16 - targetSdkVersion 27 + targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -46,6 +46,6 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 9fbcae13f..f64f65858 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -14,8 +14,8 @@ FlutterApplication and put your custom class here. --> + android:label="parse_sdk" + android:icon="@mipmap/parse"> _MyAppState(); } -class _MyAppState extends State { +class _MyAppState extends State with AutomaticKeepAliveClientMixin { DietPlanRepository dietPlanRepo; UserRepository userRepo; + // Map _result; + List> _result = []; + String info = ""; String text = ''; - + LiveQuery liveQuery; @override void initState() { super.initState(); initData(); } + @override + bool get wantKeepAlive => true; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text(text), - ), - ), + appBar: AppBar( + title: const Text('Parse sdk live test'), + ), + body: Container( + margin: EdgeInsets.all(10.0), + child: Column( + children: [ + // JsonTable( + // jsonDecode(_result.isEmpty ? "[{}]" : _result[0].toString()), + // tableHeaderBuilder: (String header) { + // return Container( + // padding: + // EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + // decoration: BoxDecoration( + // border: Border.all(width: 0.5), + // color: Colors.grey[300]), + // child: Text( + // header, + // textAlign: TextAlign.center, + // style: Theme.of(context).textTheme.display1.copyWith( + // fontWeight: FontWeight.w700, + // fontSize: 14.0, + // color: Colors.black87), + // ), + // ); + // }, + // tableCellBuilder: (dynamic value) { + // return Container( + // padding: + // EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), + // decoration: BoxDecoration( + // border: Border.all( + // width: 0.5, color: Colors.grey.withOpacity(0.5))), + // child: Text( + // value, + // textAlign: TextAlign.center, + // style: Theme.of(context) + // .textTheme + // .display1 + // .copyWith(fontSize: 14.0, color: Colors.grey[900]), + // ), + // ); + // }, + // ), + Flexible( + child: ListView( + children: + _result.map((location) => _ResultItem(location)).toList(), + )), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Button(label: '开始监听', onPressed: _listen()), + RaisedButton( + onPressed: () { + updateSingleItem(context); + }, + color: Colors.blue[400], + child: new Text('更新', + style: new TextStyle(color: Colors.white))), + ], + ), + // RaisedButton( + // onPressed: () { + // _listen(); + // }, + // color: Colors.blue[400], + // child: + // new Text('开始监听', style: new TextStyle(color: Colors.white))), + // RaisedButton( + // onPressed: () { + // _change(context); + // }, + // color: Colors.blue[400], + // child: + // new Text('修改数据', style: new TextStyle(color: Colors.white))), + ], + ), + )), ); } + Future _listen() async { + QueryBuilder query = QueryBuilder(DietPlan()) + ..whereEqualTo('objectId', 'jSH0CHYwjL'); + // LiveQuery liveQuery = LiveQuery(); + // print("=====query: $query"); + await liveQuery.subscribe(query); + + liveQuery.on(LiveQueryEvent.update, (dynamic value) { + print("监听数据连接成功,开始订阅消息!"); + + print('*** UPDATE ***: ${DateTime.now().toString()}\n $value'); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + // print((value as ParseObject).get('objectId')); + // print((value as ParseObject).get('updatedAt')); + // print((value as ParseObject).get('createdAt')); + + print("监听到数据变化:" + (value as ParseObject).toJson().toString()); + // _result.add(value); + }); + } + + Future updateSingleItem(BuildContext context) async { + final ParseResponse apiResponse = await DietPlan().getObject('jSH0CHYwjL'); + + if (apiResponse.success && apiResponse.count > 0) { + final DietPlan dietPlan = apiResponse.result; + + // Shows example of storing values in their proper type and retrieving them + var s = new WordPair.random().asPascalCase; + dietPlan.set('Name', s); + await dietPlan.save(); + // await createItem(); + // Shows example of pinning an item + // await dietPlan.pin(); + + // shows example of retrieving a pin + setState(() {}); + } else { + print(keyAppName + ': ' + apiResponse.error.message); + } + } + Future initData() async { // Initialize repository await initRepository(); @@ -69,8 +194,15 @@ class _MyAppState extends State { // Initialize parse await Parse().initialize(keyParseApplicationId, keyParseServerUrl, masterKey: keyParseMasterKey, + liveQueryUrl: keyParseLiveServerUrl, + // clientKey: "XXXi3GejX3SIxpDgSbKHHV8uHUUP3QGiPPTlxxxx", + sessionId: "1212121", + autoSendSessionId: true, debug: true, coreStore: await CoreStoreSharedPrefsImp.getInstance()); + // ParseHTTPClient client = ParseHTTPClient(); + + liveQuery = LiveQuery(); //parse serve with secure store and desktop support @@ -83,9 +215,31 @@ class _MyAppState extends State { final ParseResponse response = await Parse().healthCheck(); if (response.success) { - await runTestQueries(); - text += 'runTestQueries\n'; - print(text); + // await _listen(); + QueryBuilder query = QueryBuilder(DietPlan()) + ..whereEqualTo('objectId', 'jSH0CHYwjL'); + // LiveQuery liveQuery = LiveQuery(); + // print("=====query: $query"); + await liveQuery.subscribe(query); + + await liveQuery.on(LiveQueryEvent.update, (dynamic value) { + print("监听数据连接成功,开始订阅消息!"); + + print('*** UPDATE ***: ${DateTime.now().toString()}\n $value'); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('Name')); + // print((value as ParseObject).get('updatedAt')); + // print((value as ParseObject).get('createdAt')); + _result.clear(); + print("监听到数据变化:" + (value as ParseObject).toJson().toString()); + _result.add(value.toJson()); + print(_result.toString()); + }); + // await runTestQueries(); + // text += 'runTestQueries\n'; + // print(text); } else { text += 'Server health check failed'; print(text); @@ -120,7 +274,7 @@ class _MyAppState extends State { } Future test() async { - User user = User('test_user', 'test_password', 'test@gmail.com'); + User user = User('unreal0', 'hhhhhh', 'unreal0@sina.cn'); final ParseResponse signUpResponse = await user.signUp(); if (signUpResponse.success) { @@ -376,7 +530,6 @@ class _MyAppState extends State { userRepo ??= UserRepository.init(await getDB()); } - /// Available options: /// SharedPreferences - Not secure but will work with older versions of SDK - CoreStoreSharedPrefsImpl /// Sembast - NoSQL DB - Has security - CoreStoreSembastImpl @@ -394,3 +547,88 @@ const String dietPlansToAdd = '{"className":"Diet_Plans","Name":"Low Carb","Description":"Low Carb diet, main focus on quality fats and protein.","Fat":35,"Carbs":25,"Protein":40,"Status":0},' '{"className":"Diet_Plans","Name":"Paleo","Description":"Paleo diet.","Fat":60,"Carbs":25,"Protein":10,"Status":0},' '{"className":"Diet_Plans","Name":"Ketogenic","Description":"High quality fats, low carbs.","Fat":65,"Carbs":5,"Protein":30,"Status":0}]'; + +class _ResultItem extends StatelessWidget { + final Map _data; + + const _ResultItem(this._data, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateTime.now().toIso8601String(), + style: TextStyle(color: Colors.grey), + ), + SizedBox(width: 4.0, height: 4.0), + // Text( + // "$_data", + // style: TextStyle(color: Colors.black87), + // ), + Text( + jsonFormat(_data), + style: TextStyle(color: Colors.grey[600]), + ), + ], + ), + ); + } + + String jsonFormat(Map json) { + JsonEncoder encoder = JsonEncoder.withIndent(' '); + return encoder.convert(json); + } +} + +// class DietPlan extends ParseObject { +// DietPlan() : super(DIET_PLAN); + +// String name; +// String description; +// num protein; +// num carbs; +// num fat; +// num status; + +// static const String DIET_PLAN = 'post'; +// static const String NAME = 'title'; +// // static const String DESCRIPTION = 'text'; +// // static const String PROTEIN = 'Protein'; +// // static const String CARBS = 'Carbs'; +// // static const String FAT = 'Fat'; +// // static const String STATUS = 'Status'; + +// @override +// dynamic fromJson(Map objectData) { +// this.name = objectData[NAME]; +// // this.description = objectData[DESCRIPTION]; +// // this.protein = objectData[PROTEIN]; +// // this.carbs = objectData[CARBS]; +// // this.fat = objectData[FAT]; +// // this.status = objectData[STATUS]; +// return this; +// } + +// // Map toJson() => { +// // NAME: name, +// // // DESCRIPTION: description, +// // // PROTEIN: protein, +// // // CARBS: carbs, +// // // FAT: fat, +// // // STATUS: status, +// // }; + +// @override +// String toString() { +// return toJson().toString(); +// } + +// @override +// dynamic copy() { +// return DietPlan(); +// } +// } diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 000000000..ddc00d9df --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,81 @@ +platform :osx, '10.13' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + # TODO: Add Profile support to projects. + #'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 + pods_ary = [] + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) { |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) + pods_ary.push({:name => podname, :path => podpath}); + else + puts "Invalid plugin specification: #{line}" + end + } + return pods_ary +end + +def pubspec_supports_macos(file) + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return false; + end + File.foreach(file_abs_path) { |line| + # TODO(stuartmorgan): Use formal platform declaration once it exists, + # see https://github.com/flutter/flutter/issues/33597. + return true if line =~ /^\s*macosPrefix:/ + } + return false +end + +target 'Runner' do + use_frameworks! + + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + ephemeral_dir = File.join('Flutter', 'ephemeral') + symlink_dir = File.join(ephemeral_dir, '.symlinks') + symlink_plugins_dir = File.join(symlink_dir, 'plugins') + system("rm -rf #{symlink_dir}") + system("mkdir -p #{symlink_plugins_dir}") + + # Flutter Pods + generated_xcconfig = parse_KV_file(File.join(ephemeral_dir, 'Flutter-Generated.xcconfig')) + if generated_xcconfig.empty? + puts "Flutter-Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." + end + generated_xcconfig.map { |p| + if p[:name] == 'FLUTTER_FRAMEWORK_DIR' + symlink = File.join(symlink_dir, 'flutter') + File.symlink(File.dirname(p[:path]), symlink) + pod 'FlutterMacOS', :path => File.join(symlink, File.basename(p[:path])) + end + } + + # Plugin Pods + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.map { |p| + symlink = File.join(symlink_plugins_dir, p[:name]) + File.symlink(p[:path], symlink) + if pubspec_supports_macos(File.join(symlink, 'pubspec.yaml')) + pod p[:name], :path => File.join(symlink, 'macos') + end + } +end diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 31b0596b5..5a3b01f47 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,6 +10,10 @@ dependencies: cupertino_icons: ^0.1.2 sembast: ^1.13.3+1 shared_preferences: ^0.5.0 + json_table: ^0.3.0 + intl: ^0.15.8 + english_words: ^3.1.4 + json_serializable: ^3.0.0 dev_dependencies: parse_server_sdk: @@ -23,7 +27,6 @@ dev_dependencies: # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. @@ -31,7 +34,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - # - images/a_dot_burr.jpeg + # - images/a_dot_burr.jpeg - assets/parse.png # An image asset can refer to one or more resolution-specific "variants", see diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 0c33147d6..72f3085f7 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -81,6 +81,8 @@ class LiveQuery { } _channel = IOWebSocketChannel(_webSocket); + // _channel = IOWebSocketChannel.connect(_liveQueryURL); + _channel.stream.listen((dynamic message) { if (_debug) { print('$_printConstLiveQuery: Listen: $message');