diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 620f1db5f..889345e65 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: sembast: ^2.0.1 shared_preferences: ^0.5.0 + path_provider: ^1.6.14 + dev_dependencies: parse_server_sdk: path: ../ diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 681143501..b84c7a83b 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -1,156 +1,198 @@ -library flutter_parse_sdk; - import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:connectivity/connectivity.dart'; import 'package:flutter/widgets.dart'; -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; -import 'package:meta/meta.dart'; import 'package:package_info/package_info.dart'; -import 'package:parse_server_sdk/src/network/parse_websocket.dart' - as parse_web_socket; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:sembast/sembast.dart'; -import 'package:sembast/sembast_io.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:uuid/uuid.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -import 'package:xxtea/xxtea.dart'; - -export 'src/utils/parse_live_list.dart'; - -part 'package:parse_server_sdk/src/data/core_store.dart'; -part 'package:parse_server_sdk/src/data/parse_subclass_handler.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_response_builder.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_response_utils.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_success_no_results.dart'; -part 'package:parse_server_sdk/src/storage/core_store_sem_impl.dart'; -part 'package:parse_server_sdk/src/storage/core_store_sp_impl.dart'; -part 'package:parse_server_sdk/src/storage/xxtea_codec.dart'; -part 'src/base/parse_constants.dart'; -part 'src/data/parse_core_data.dart'; -part 'src/enums/parse_enum_api_rq.dart'; -part 'src/network/parse_http_client.dart'; -part 'src/network/parse_live_query.dart'; -part 'src/network/parse_query.dart'; -part 'src/objects/parse_acl.dart'; -part 'src/objects/parse_base.dart'; -part 'src/objects/parse_cloneable.dart'; -part 'src/objects/parse_config.dart'; -part 'src/objects/parse_error.dart'; -part 'src/objects/parse_file.dart'; -part 'src/objects/parse_file_base.dart'; -part 'src/objects/parse_file_web.dart'; -part 'src/objects/parse_function.dart'; -part 'src/objects/parse_geo_point.dart'; -part 'src/objects/parse_installation.dart'; -part 'src/objects/parse_merge.dart'; -part 'src/objects/parse_object.dart'; -part 'src/objects/parse_relation.dart'; -part 'src/objects/parse_response.dart'; -part 'src/objects/parse_session.dart'; -part 'src/objects/parse_user.dart'; -part 'src/utils/parse_date_format.dart'; -part 'src/utils/parse_decoder.dart'; -part 'src/utils/parse_encoder.dart'; -part 'src/utils/parse_file_extensions.dart'; -part 'src/utils/parse_logger.dart'; -part 'src/utils/parse_login_helpers.dart'; -part 'src/utils/parse_utils.dart'; - -class Parse { - ParseCoreData data; - bool _hasBeenInitialized = false; +import 'parse_server_sdk_dart.dart' as sdk; +import 'src/storage/core_store_sp_impl.dart'; + +export 'parse_server_sdk_dart.dart' hide Parse, CoreStoreSembastImp; +export 'src/storage/core_store_sp_impl.dart'; +export 'src/utils/parse_live_list_flutter.dart'; + +class Parse extends sdk.Parse + with WidgetsBindingObserver + implements sdk.ParseConnectivityProvider { /// To initialize Parse Server in your application /// /// This should be initialized in MyApp() creation /// /// ``` /// Parse().initialize( - // "PARSE_APP_ID", - // "https://parse.myaddress.com/parse/, - // masterKey: "asd23rjh234r234r234r", - // debug: true, - // liveQuery: true); - // ``` + /// "PARSE_APP_ID", + /// "https://parse.myaddress.com/parse/, + /// masterKey: "asd23rjh234r234r234r", + /// debug: true, + /// liveQuery: true); + /// ``` + /// [appName], [appVersion] and [appPackageName] are automatically set on Android and IOS, if they are not defined. You should provide a value on web. + /// [fileDirectory] is not used on web + @override Future initialize( String appId, String serverUrl, { bool debug = false, - String appName = '', + String appName, + String appVersion, + String appPackageName, + String locale, String liveQueryUrl, String clientKey, String masterKey, String sessionId, bool autoSendSessionId, SecurityContext securityContext, - CoreStore coreStore, - Map registeredSubClassMap, - ParseUserConstructor parseUserConstructor, - ParseFileConstructor parseFileConstructor, + sdk.CoreStore coreStore, + Map registeredSubClassMap, + sdk.ParseUserConstructor parseUserConstructor, + sdk.ParseFileConstructor parseFileConstructor, List liveListRetryIntervals, + sdk.ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, }) async { - final String url = removeTrailingSlash(serverUrl); + if (!sdk.parseIsWeb && (appName == null || appVersion == null || appPackageName == null)) { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + appName ??= packageInfo.appName; + appVersion ??= packageInfo.version; + appPackageName ??= packageInfo.packageName; + } - await ParseCoreData.init( + return await super.initialize( appId, - url, + serverUrl, debug: debug, appName: appName, + appVersion: appVersion, + appPackageName: appPackageName, + locale: locale ?? sdk.parseIsWeb + ? ui.window.locale.toString() + : Platform.localeName, liveQueryUrl: liveQueryUrl, - masterKey: masterKey, clientKey: clientKey, + masterKey: masterKey, sessionId: sessionId, autoSendSessionId: autoSendSessionId, securityContext: securityContext, - store: coreStore, + coreStore: coreStore ?? + await CoreStoreSharedPrefsImp.getInstance(password: masterKey), registeredSubClassMap: registeredSubClassMap, parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, - liveListRetryIntervals: liveListRetryIntervals, + connectivityProvider: connectivityProvider ?? this, + fileDirectory: fileDirectory ?? (await getTemporaryDirectory()).path, + appResumedStream: appResumedStream ?? _appResumedStreamController.stream, ); + } - _hasBeenInitialized = true; + final StreamController _appResumedStreamController = + StreamController(); + + @override + Future checkConnectivity() async { + //Connectivity works differently on web + if (!sdk.parseIsWeb) { + switch (await Connectivity().checkConnectivity()) { + case ConnectivityResult.wifi: + return sdk.ParseConnectivityResult.wifi; + case ConnectivityResult.mobile: + return sdk.ParseConnectivityResult.mobile; + case ConnectivityResult.none: + return sdk.ParseConnectivityResult.none; + } + } + return sdk.ParseConnectivityResult.wifi; + } - return this; + @override + Stream get connectivityStream { + return Connectivity().onConnectivityChanged.map((ConnectivityResult event) { + switch (event) { + case ConnectivityResult.wifi: + return sdk.ParseConnectivityResult.wifi; + case ConnectivityResult.mobile: + return sdk.ParseConnectivityResult.mobile; + default: + return sdk.ParseConnectivityResult.none; + } + }); } - bool hasParseBeenInitialized() => _hasBeenInitialized; + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _appResumedStreamController.sink.add(null); + } +} - Future healthCheck( - {bool debug, ParseHTTPClient client, bool sendSessionIdByDefault}) async { - ParseResponse parseResponse; +class CoreStoreSembastImp implements sdk.CoreStoreSembastImp { + CoreStoreSembastImp._(); + + static sdk.CoreStoreSembastImp _sembastImp; + + static Future getInstance(String dbPath, + {DatabaseFactory factory, String password}) async { + if (_sembastImp == null) { + String dbDirectory = ''; + if (!sdk.parseIsWeb && + (Platform.isIOS || Platform.isAndroid || Platform.isMacOS)) + dbDirectory = (await getApplicationDocumentsDirectory()).path; + final String dbPath = path.join('$dbDirectory/parse', 'parse.db'); + _sembastImp ??= await sdk.CoreStoreSembastImp.getInstance(dbPath, + factory: factory, password: password); + } + return CoreStoreSembastImp._(); + } - final bool _debug = isDebugEnabled(objectLevelDebug: debug); + @override + Future clear() => _sembastImp.clear(); - final ParseHTTPClient _client = client ?? - ParseHTTPClient( - sendSessionId: - sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, - securityContext: ParseCoreData().securityContext); + @override + Future containsKey(String key) => _sembastImp.containsKey(key); - const String className = 'parseBase'; - const ParseApiRQ type = ParseApiRQ.healthCheck; + @override + Future get(String key) => _sembastImp.get(key); - try { - final Response response = - await _client.get('${ParseCoreData().serverUrl}$keyEndPointHealth'); - parseResponse = - handleResponse(null, response, type, _debug, className); - } on Exception catch (e) { - parseResponse = handleException(e, type, _debug, className); - } + @override + Future getBool(String key) => _sembastImp.getBool(key); - return parseResponse; - } + @override + Future getDouble(String key) => _sembastImp.getDouble(key); + + @override + Future getInt(String key) => _sembastImp.getInt(key); + + @override + Future getString(String key) => _sembastImp.getString(key); + + @override + Future> getStringList(String key) => + _sembastImp.getStringList(key); + + @override + Future remove(String key) => _sembastImp.remove(key); + + @override + Future setBool(String key, bool value) => + _sembastImp.setBool(key, value); + + @override + Future setDouble(String key, double value) => + _sembastImp.setDouble(key, value); + + @override + Future setInt(String key, int value) => _sembastImp.setInt(key, value); + + @override + Future setString(String key, String value) => + _sembastImp.setString(key, value); + + @override + Future setStringList(String key, List values) => + _sembastImp.setStringList(key, values); } diff --git a/lib/parse_server_sdk_dart.dart b/lib/parse_server_sdk_dart.dart new file mode 100644 index 000000000..1da9ae8ae --- /dev/null +++ b/lib/parse_server_sdk_dart.dart @@ -0,0 +1,161 @@ +library flutter_parse_sdk; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:meta/meta.dart'; +import 'package:parse_server_sdk/src/network/parse_websocket.dart' + as parse_web_socket; +import 'package:path/path.dart' as path; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:uuid/uuid.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:xxtea/xxtea.dart'; + +part 'package:parse_server_sdk/src/data/core_store.dart'; +part 'package:parse_server_sdk/src/data/parse_subclass_handler.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_response_builder.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_response_utils.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_success_no_results.dart'; +part 'package:parse_server_sdk/src/storage/core_store_sem_impl.dart'; +part 'package:parse_server_sdk/src/storage/xxtea_codec.dart'; +part 'src/base/parse_constants.dart'; +part 'src/data/parse_core_data.dart'; +part 'src/enums/parse_enum_api_rq.dart'; +part 'src/network/parse_connectivity.dart'; +part 'src/network/parse_http_client.dart'; +part 'src/network/parse_live_query.dart'; +part 'src/network/parse_query.dart'; +part 'src/objects/parse_acl.dart'; +part 'src/objects/parse_base.dart'; +part 'src/objects/parse_cloneable.dart'; +part 'src/objects/parse_config.dart'; +part 'src/objects/parse_error.dart'; +part 'src/objects/parse_file.dart'; +part 'src/objects/parse_file_base.dart'; +part 'src/objects/parse_file_web.dart'; +part 'src/objects/parse_function.dart'; +part 'src/objects/parse_geo_point.dart'; +part 'src/objects/parse_installation.dart'; +part 'src/objects/parse_merge.dart'; +part 'src/objects/parse_object.dart'; +part 'src/objects/parse_relation.dart'; +part 'src/objects/parse_response.dart'; +part 'src/objects/parse_session.dart'; +part 'src/objects/parse_user.dart'; +part 'src/utils/parse_date_format.dart'; +part 'src/utils/parse_decoder.dart'; +part 'src/utils/parse_encoder.dart'; +part 'src/utils/parse_file_extensions.dart'; +part 'src/utils/parse_logger.dart'; +part 'src/utils/parse_login_helpers.dart'; +part 'src/utils/parse_utils.dart'; +part 'src/utils/parse_live_list.dart'; + +class Parse { + ParseCoreData data; + bool _hasBeenInitialized = false; + + /// To initialize Parse Server in your application + /// + /// This should be initialized in MyApp() creation + /// + /// ``` + /// Parse().initialize( + /// "PARSE_APP_ID", + /// "https://parse.myaddress.com/parse/, + /// masterKey: "asd23rjh234r234r234r", + /// debug: true, + /// liveQuery: true); + /// ``` + Future initialize( + String appId, + String serverUrl, { + bool debug = false, + String appName, + String appVersion, + String appPackageName, + String locale, + String liveQueryUrl, + String clientKey, + String masterKey, + String sessionId, + bool autoSendSessionId, + SecurityContext securityContext, + CoreStore coreStore, + Map registeredSubClassMap, + ParseUserConstructor parseUserConstructor, + ParseFileConstructor parseFileConstructor, + List liveListRetryIntervals, + ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, + }) async { + final String url = removeTrailingSlash(serverUrl); + + await ParseCoreData.init( + appId, + url, + debug: debug, + appName: appName, + appVersion: appVersion, + appPackageName: appPackageName, + locale: locale, + liveQueryUrl: liveQueryUrl, + masterKey: masterKey, + clientKey: clientKey, + sessionId: sessionId, + autoSendSessionId: autoSendSessionId, + securityContext: securityContext, + store: coreStore, + registeredSubClassMap: registeredSubClassMap, + parseUserConstructor: parseUserConstructor, + parseFileConstructor: parseFileConstructor, + liveListRetryIntervals: liveListRetryIntervals, + connectivityProvider: connectivityProvider, + fileDirectory: fileDirectory, + appResumedStream: appResumedStream, + ); + + _hasBeenInitialized = true; + + return this; + } + + bool hasParseBeenInitialized() => _hasBeenInitialized; + + Future healthCheck( + {bool debug, ParseHTTPClient client, bool sendSessionIdByDefault}) async { + ParseResponse parseResponse; + + final bool _debug = isDebugEnabled(objectLevelDebug: debug); + + final ParseHTTPClient _client = client ?? + ParseHTTPClient( + sendSessionId: + sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); + + const String className = 'parseBase'; + const ParseApiRQ type = ParseApiRQ.healthCheck; + + try { + final Response response = + await _client.get('${ParseCoreData().serverUrl}$keyEndPointHealth'); + parseResponse = + handleResponse(null, response, type, _debug, className); + } on Exception catch (e) { + parseResponse = handleException(e, type, _debug, className); + } + + return parseResponse; + } +} diff --git a/lib/src/data/parse_core_data.dart b/lib/src/data/parse_core_data.dart index 02b37c2af..514b83dd5 100644 --- a/lib/src/data/parse_core_data.dart +++ b/lib/src/data/parse_core_data.dart @@ -19,6 +19,9 @@ class ParseCoreData { String serverUrl, { bool debug, String appName, + String appVersion, + String appPackageName, + String locale, String liveQueryUrl, String masterKey, String clientKey, @@ -30,11 +33,16 @@ class ParseCoreData { ParseUserConstructor parseUserConstructor, ParseFileConstructor parseFileConstructor, List liveListRetryIntervals, + ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, }) async { _instance = ParseCoreData._init(appId, serverUrl); - _instance.storage ??= - store ?? await CoreStoreSharedPrefsImp.getInstance(password: masterKey); + assert(_instance.storage != null || store != null, + 'There is no CoreStore set.'); + + _instance.storage ??= store; if (debug != null) { _instance.debug = debug; @@ -42,6 +50,15 @@ class ParseCoreData { if (appName != null) { _instance.appName = appName; } + if (appVersion != null) { + _instance.appVersion = appVersion; + } + if (appPackageName != null) { + _instance.appPackageName = appPackageName; + } + if (locale != null) { + _instance.locale = locale; + } if (liveQueryUrl != null) { _instance.liveQueryURL = liveQueryUrl; } @@ -73,10 +90,24 @@ class ParseCoreData { parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, ); + if (connectivityProvider != null) { + _instance.connectivityProvider = connectivityProvider; + } + + if (fileDirectory != null) { + _instance.fileDirectory = fileDirectory; + } + + if(appResumedStream!= null){ + _instance.appResumedStream = appResumedStream; + } } String appName; + String appVersion; + String appPackageName; String applicationId; + String locale; String serverUrl; String liveQueryURL; String masterKey; @@ -88,6 +119,9 @@ class ParseCoreData { CoreStore storage; ParseSubClassHandler _subClassHandler; List liveListRetryIntervals; + ParseConnectivityProvider connectivityProvider; + String fileDirectory; + Stream appResumedStream; void registerSubClass( String className, ParseObjectConstructor objectConstructor) { diff --git a/lib/src/network/parse_connectivity.dart b/lib/src/network/parse_connectivity.dart new file mode 100644 index 000000000..731ad1a4b --- /dev/null +++ b/lib/src/network/parse_connectivity.dart @@ -0,0 +1,18 @@ +part of flutter_parse_sdk; + +/// Connection status check result. +enum ParseConnectivityResult { + /// WiFi: Device connected via Wi-Fi + wifi, + + /// Mobile: Device connected to cellular network + mobile, + + /// None: Device not connected to any network + none +} + +abstract class ParseConnectivityProvider { + Future checkConnectivity(); + Stream get connectivityStream; +} diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 05e6be9a4..cd2b861be 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -33,15 +33,20 @@ class Subscription { enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } -class LiveQueryReconnectingController with WidgetsBindingObserver { +class LiveQueryReconnectingController { LiveQueryReconnectingController( - this._reconnect, this._eventStream, this.debug) { - //Connectivity works differently on web - if (!parseIsWeb) { - Connectivity().checkConnectivity().then(_connectivityChanged); - Connectivity().onConnectivityChanged.listen(_connectivityChanged); + this._reconnect, + this._eventStream, + this.debug, + ) { + final ParseConnectivityProvider connectivityProvider = + ParseCoreData().connectivityProvider; + if (connectivityProvider != null) { + connectivityProvider.checkConnectivity().then(_connectivityChanged); + connectivityProvider.connectivityStream.listen(_connectivityChanged); } else { - _connectivityChanged(ConnectivityResult.wifi); + print( + 'LiveQuery does not work, if there is ParseConnectivityProvider provided.'); } _eventStream.listen((LiveQueryClientEvent event) { switch (event) { @@ -67,7 +72,7 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { print('$DEBUG_TAG: $event'); } }); - WidgetsBinding.instance.addObserver(this); + ParseCoreData().appResumedStream?.listen((void _) => _setReconnect()); } static List get retryInterval => ParseCoreData().liveListRetryIntervals; @@ -84,28 +89,17 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { Timer _currentTimer; - void _connectivityChanged(ConnectivityResult state) { - if (!_isOnline && state != ConnectivityResult.none) { + void _connectivityChanged(ParseConnectivityResult state) { + if (!_isOnline && state != ParseConnectivityResult.none) { _retryState = 0; } - _isOnline = state != ConnectivityResult.none; + _isOnline = state != ParseConnectivityResult.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 && diff --git a/lib/src/objects/parse_file.dart b/lib/src/objects/parse_file.dart index c44d63ab3..a149839df 100644 --- a/lib/src/objects/parse_file.dart +++ b/lib/src/objects/parse_file.dart @@ -21,14 +21,12 @@ class ParseFile extends ParseFileBase { File file; Future loadStorage() async { - final Directory tempPath = await getTemporaryDirectory(); - if (name == null) { file = null; return this; } - final File possibleFile = File('${tempPath.path}/$name'); + final File possibleFile = File('${ParseCoreData().fileDirectory}/$name'); // ignore: avoid_slow_async_io final bool exists = await possibleFile.exists(); @@ -47,8 +45,7 @@ class ParseFile extends ParseFileBase { return this; } - final Directory tempPath = await getTemporaryDirectory(); - file = File('${tempPath.path}/$name'); + file = File('${ParseCoreData().fileDirectory}/$name'); await file.create(); final Response response = await _client.get(url); await file.writeAsBytes(response.bodyBytes); diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index ad44b3470..c30937742 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -83,22 +83,14 @@ class ParseInstallation extends ParseObject { } //Locale - final String locale = parseIsWeb - ? ui.window.locale.toString() - : Platform.localeName; - if (locale != null && locale.isNotEmpty) { - set(keyLocaleIdentifier, locale); - } + set(keyLocaleIdentifier, ParseCoreData().locale); //Timezone //App info - if (!parseIsWeb) { - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - set(keyAppName, packageInfo.appName); - set(keyAppVersion, packageInfo.version); - set(keyAppIdentifier, packageInfo.packageName); - } + set(keyAppName, ParseCoreData().appName); + set(keyAppVersion, ParseCoreData().appVersion); + set(keyAppIdentifier, ParseCoreData().appPackageName); set(keyParseVersion, keySdkVersion); } diff --git a/lib/src/storage/core_store_sem_impl.dart b/lib/src/storage/core_store_sem_impl.dart index f8e2c317d..bf60602bd 100644 --- a/lib/src/storage/core_store_sem_impl.dart +++ b/lib/src/storage/core_store_sem_impl.dart @@ -6,16 +6,11 @@ class CoreStoreSembastImp implements CoreStore { static CoreStoreSembastImp _instance; - static Future getInstance( + static Future getInstance(String dbPath, {DatabaseFactory factory, String password = 'flutter_sdk'}) async { if (_instance == null) { factory ??= databaseFactoryIo; final SembastCodec codec = getXXTeaSembastCodec(password: password); - String dbDirectory = ''; - if (!parseIsWeb && - (Platform.isIOS || Platform.isAndroid || Platform.isMacOS)) - dbDirectory = (await getApplicationDocumentsDirectory()).path; - final String dbPath = path.join('$dbDirectory/parse', 'parse.db'); final Database db = await factory.openDatabase(dbPath, codec: codec); _instance = CoreStoreSembastImp._internal(db, StoreRef.main()); diff --git a/lib/src/storage/core_store_sp_impl.dart b/lib/src/storage/core_store_sp_impl.dart index c09e6970a..e41693c43 100644 --- a/lib/src/storage/core_store_sp_impl.dart +++ b/lib/src/storage/core_store_sp_impl.dart @@ -1,4 +1,5 @@ -part of flutter_parse_sdk; +import 'package:parse_server_sdk/parse_server_sdk_dart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class CoreStoreSharedPrefsImp implements CoreStore { CoreStoreSharedPrefsImp._internal(this._store); diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 0528e676d..827e27f6b 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import '../../parse_server_sdk.dart'; +part of flutter_parse_sdk; // ignore_for_file: invalid_use_of_protected_member class ParseLiveList { @@ -335,8 +331,8 @@ class ParseLiveList { includeList.add(key); // ignore: avoid_as if ((includes[key] as Map).isNotEmpty) { - includeList - .addAll(_toIncludeStringList(includes[key]).map((String e) => '$key.$e')); + includeList.addAll( + _toIncludeStringList(includes[key]).map((String e) => '$key.$e')); } } return includeList; @@ -678,8 +674,8 @@ class ParseLiveListElement { ..keysToReturn([keyVarUpdatedAt]) ..whereEqualTo(keyVarObjectId, subObject.objectId); final ParseResponse parseResponse = await queryBuilder.query(); - if (parseResponse.success && parseResponse.results.first.updatedAt != - subObject.updatedAt) { + if (parseResponse.success && + parseResponse.results.first.updatedAt != subObject.updatedAt) { queryBuilder.limiters.remove('keys'); queryBuilder.includeObject(_getIncludeList(path[key])); final ParseResponse parseResponse = await queryBuilder.query(); @@ -735,8 +731,7 @@ class ParseLiveListDeleteEvent typedef StreamGetter = Stream Function(); typedef DataGetter = T Function(); -typedef ChildBuilder = Widget Function( - BuildContext context, ParseLiveListElementSnapshot snapshot); + class ParseLiveListElementSnapshot { ParseLiveListElementSnapshot({this.loadedData, this.error}); @@ -749,240 +744,4 @@ class ParseLiveListElementSnapshot { bool get failed => error != null; } -class ParseLiveListWidget extends StatefulWidget { - const ParseLiveListWidget({ - Key key, - @required this.query, - this.listLoadingElement, - this.duration = const Duration(milliseconds: 300), - this.scrollPhysics, - this.scrollController, - this.scrollDirection = Axis.vertical, - this.padding, - this.primary, - this.reverse = false, - this.childBuilder, - this.shrinkWrap = false, - this.removedItemBuilder, - this.listenOnAllSubItems, - this.listeningIncludes, - this.lazyLoading = true, - }) : super(key: key); - - final QueryBuilder query; - final Widget listLoadingElement; - final Duration duration; - final ScrollPhysics scrollPhysics; - final ScrollController scrollController; - - final Axis scrollDirection; - final EdgeInsetsGeometry padding; - final bool primary; - final bool reverse; - final bool shrinkWrap; - - final ChildBuilder childBuilder; - final ChildBuilder removedItemBuilder; - - final bool listenOnAllSubItems; - final List listeningIncludes; - - final bool lazyLoading; - - @override - _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( - query: query, - removedItemBuilder: removedItemBuilder, - listenOnAllSubItems: listenOnAllSubItems, - listeningIncludes: listeningIncludes, - lazyLoading: lazyLoading, - ); - - static Widget defaultChildBuilder( - BuildContext context, ParseLiveListElementSnapshot snapshot) { - Widget child; - if (snapshot.failed) { - child = const Text('something went wrong!'); - } else if (snapshot.hasData) { - child = ListTile( - title: Text( - snapshot.loadedData.get(keyVarObjectId), - ), - ); - } else { - child = const ListTile( - leading: CircularProgressIndicator(), - ); - } - return child; - } -} - -class _ParseLiveListWidgetState - extends State> { - _ParseLiveListWidgetState( - {@required this.query, - @required this.removedItemBuilder, - bool listenOnAllSubItems, - List listeningIncludes, - bool lazyLoading = true}) { - ParseLiveList.create( - query, - listenOnAllSubItems: listenOnAllSubItems, - listeningIncludes: listeningIncludes, - lazyLoading: lazyLoading, - ).then((ParseLiveList value) { - setState(() { - _liveList = value; - _liveList.stream.listen((ParseLiveListEvent event) { - if (event is ParseLiveListAddEvent) { - if (_animatedListKey.currentState != null) - _animatedListKey.currentState - .insertItem(event.index, duration: widget.duration); - } else if (event is ParseLiveListDeleteEvent) { - _animatedListKey.currentState.removeItem( - event.index, - (BuildContext context, Animation animation) => - ParseLiveListElementWidget( - key: ValueKey(event.object?.get( - keyVarObjectId, - defaultValue: 'removingItem')), - childBuilder: widget.childBuilder ?? - ParseLiveListWidget.defaultChildBuilder, - sizeFactor: animation, - duration: widget.duration, - loadedData: () => event.object, - ), - duration: widget.duration); - } - }); - }); - }); - } - - final QueryBuilder query; - ParseLiveList _liveList; - final GlobalKey _animatedListKey = - GlobalKey(); - final ChildBuilder removedItemBuilder; - @override - Widget build(BuildContext context) { - return _liveList == null - ? widget.listLoadingElement ?? Container() - : buildAnimatedList(); - } - - Widget buildAnimatedList() { - return AnimatedList( - key: _animatedListKey, - physics: widget.scrollPhysics, - controller: widget.scrollController, - scrollDirection: widget.scrollDirection, - padding: widget.padding, - primary: widget.primary, - reverse: widget.reverse, - shrinkWrap: widget.shrinkWrap, - initialItemCount: _liveList?.size, - itemBuilder: - (BuildContext context, int index, Animation animation) { - return ParseLiveListElementWidget( - key: ValueKey( - _liveList?.getIdentifier(index) ?? '_NotFound'), - stream: () => _liveList?.getAt(index), - loadedData: () => _liveList?.getLoadedAt(index), - sizeFactor: animation, - duration: widget.duration, - childBuilder: - widget.childBuilder ?? ParseLiveListWidget.defaultChildBuilder, - ); - }); - } - - @override - void dispose() { - _liveList?.dispose(); - _liveList = null; - super.dispose(); - } -} - -class ParseLiveListElementWidget extends StatefulWidget { - const ParseLiveListElementWidget( - {Key key, - this.stream, - this.loadedData, - @required this.sizeFactor, - @required this.duration, - @required this.childBuilder}) - : super(key: key); - - final StreamGetter stream; - final DataGetter loadedData; - final Animation sizeFactor; - final Duration duration; - final ChildBuilder childBuilder; - - @override - _ParseLiveListElementWidgetState createState() { - return _ParseLiveListElementWidgetState(loadedData, stream); - } -} - -class _ParseLiveListElementWidgetState - extends State> - with SingleTickerProviderStateMixin { - _ParseLiveListElementWidgetState( - DataGetter loadedDataGetter, StreamGetter stream) { - _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); - if (stream != null) { - _streamSubscription = stream().listen( - (T data) { - if (widget != null) { - setState(() { - _snapshot = ParseLiveListElementSnapshot(loadedData: data); - }); - } else { - _snapshot = ParseLiveListElementSnapshot(loadedData: data); - } - }, - onError: (Object error) { - if (error is ParseError) { - if (widget != null) { - setState(() { - _snapshot = ParseLiveListElementSnapshot(error: error); - }); - } else { - _snapshot = ParseLiveListElementSnapshot(error: error); - } - } - }, - cancelOnError: false, - ); - } - } - - ParseLiveListElementSnapshot _snapshot; - - StreamSubscription _streamSubscription; - - @override - void dispose() { - _streamSubscription?.cancel(); - _streamSubscription = null; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Widget result = SizeTransition( - sizeFactor: widget.sizeFactor, - child: AnimatedSize( - duration: widget.duration, - vsync: this, - child: widget.childBuilder(context, _snapshot), - ), - ); - return result; - } -} diff --git a/lib/src/utils/parse_live_list_flutter.dart b/lib/src/utils/parse_live_list_flutter.dart new file mode 100644 index 000000000..1d6eb394a --- /dev/null +++ b/lib/src/utils/parse_live_list_flutter.dart @@ -0,0 +1,245 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk/parse_server_sdk_dart.dart'; + +typedef ChildBuilder = Widget Function( + BuildContext context, ParseLiveListElementSnapshot snapshot); + +class ParseLiveListWidget extends StatefulWidget { + const ParseLiveListWidget({ + Key key, + @required this.query, + this.listLoadingElement, + this.duration = const Duration(milliseconds: 300), + this.scrollPhysics, + this.scrollController, + this.scrollDirection = Axis.vertical, + this.padding, + this.primary, + this.reverse = false, + this.childBuilder, + this.shrinkWrap = false, + this.removedItemBuilder, + this.listenOnAllSubItems, + this.listeningIncludes, + this.lazyLoading = true, + }) : super(key: key); + + final QueryBuilder query; + final Widget listLoadingElement; + final Duration duration; + final ScrollPhysics scrollPhysics; + final ScrollController scrollController; + + final Axis scrollDirection; + final EdgeInsetsGeometry padding; + final bool primary; + final bool reverse; + final bool shrinkWrap; + + final ChildBuilder childBuilder; + final ChildBuilder removedItemBuilder; + + final bool listenOnAllSubItems; + final List listeningIncludes; + + final bool lazyLoading; + + @override + _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( + query: query, + removedItemBuilder: removedItemBuilder, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + ); + + static Widget defaultChildBuilder( + BuildContext context, ParseLiveListElementSnapshot snapshot) { + Widget child; + if (snapshot.failed) { + child = const Text('something went wrong!'); + } else if (snapshot.hasData) { + child = ListTile( + title: Text( + snapshot.loadedData.get(keyVarObjectId), + ), + ); + } else { + child = const ListTile( + leading: CircularProgressIndicator(), + ); + } + return child; + } +} + +class _ParseLiveListWidgetState + extends State> { + _ParseLiveListWidgetState( + {@required this.query, + @required this.removedItemBuilder, + bool listenOnAllSubItems, + List listeningIncludes, + bool lazyLoading = true}) { + ParseLiveList.create( + query, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + ).then((ParseLiveList value) { + setState(() { + _liveList = value; + _liveList.stream.listen((ParseLiveListEvent event) { + if (event is ParseLiveListAddEvent) { + if (_animatedListKey.currentState != null) + _animatedListKey.currentState + .insertItem(event.index, duration: widget.duration); + } else if (event is ParseLiveListDeleteEvent) { + _animatedListKey.currentState.removeItem( + event.index, + (BuildContext context, Animation animation) => + ParseLiveListElementWidget( + key: ValueKey(event.object?.get( + keyVarObjectId, + defaultValue: 'removingItem')), + childBuilder: widget.childBuilder ?? + ParseLiveListWidget.defaultChildBuilder, + sizeFactor: animation, + duration: widget.duration, + loadedData: () => event.object, + ), + duration: widget.duration); + } + }); + }); + }); + } + + final QueryBuilder query; + ParseLiveList _liveList; + final GlobalKey _animatedListKey = + GlobalKey(); + final ChildBuilder removedItemBuilder; + + @override + Widget build(BuildContext context) { + return _liveList == null + ? widget.listLoadingElement ?? Container() + : buildAnimatedList(); + } + + Widget buildAnimatedList() { + return AnimatedList( + key: _animatedListKey, + physics: widget.scrollPhysics, + controller: widget.scrollController, + scrollDirection: widget.scrollDirection, + padding: widget.padding, + primary: widget.primary, + reverse: widget.reverse, + shrinkWrap: widget.shrinkWrap, + initialItemCount: _liveList?.size, + itemBuilder: + (BuildContext context, int index, Animation animation) { + return ParseLiveListElementWidget( + key: ValueKey( + _liveList?.getIdentifier(index) ?? '_NotFound'), + stream: () => _liveList?.getAt(index), + loadedData: () => _liveList?.getLoadedAt(index), + sizeFactor: animation, + duration: widget.duration, + childBuilder: + widget.childBuilder ?? ParseLiveListWidget.defaultChildBuilder, + ); + }); + } + + @override + void dispose() { + _liveList?.dispose(); + _liveList = null; + super.dispose(); + } +} + +class ParseLiveListElementWidget extends StatefulWidget { + const ParseLiveListElementWidget( + {Key key, + this.stream, + this.loadedData, + @required this.sizeFactor, + @required this.duration, + @required this.childBuilder}) + : super(key: key); + + final StreamGetter stream; + final DataGetter loadedData; + final Animation sizeFactor; + final Duration duration; + final ChildBuilder childBuilder; + + @override + _ParseLiveListElementWidgetState createState() { + return _ParseLiveListElementWidgetState(loadedData, stream); + } +} + +class _ParseLiveListElementWidgetState + extends State> + with SingleTickerProviderStateMixin { + _ParseLiveListElementWidgetState( + DataGetter loadedDataGetter, StreamGetter stream) { + _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); + if (stream != null) { + _streamSubscription = stream().listen( + (T data) { + if (widget != null) { + setState(() { + _snapshot = ParseLiveListElementSnapshot(loadedData: data); + }); + } else { + _snapshot = ParseLiveListElementSnapshot(loadedData: data); + } + }, + onError: (Object error) { + if (error is ParseError) { + if (widget != null) { + setState(() { + _snapshot = ParseLiveListElementSnapshot(error: error); + }); + } else { + _snapshot = ParseLiveListElementSnapshot(error: error); + } + } + }, + cancelOnError: false, + ); + } + } + + ParseLiveListElementSnapshot _snapshot; + + StreamSubscription _streamSubscription; + + @override + void dispose() { + _streamSubscription?.cancel(); + _streamSubscription = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Widget result = SizeTransition( + sizeFactor: widget.sizeFactor, + child: AnimatedSize( + duration: widget.duration, + vsync: this, + child: widget.childBuilder(context, _snapshot), + ), + ); + return result; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b23f9ab4a..50a5bfed7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,17 +13,17 @@ dependencies: # Networking web_socket_channel: ^1.1.0 - connectivity: ^0.4.9+2 + connectivity: ^0.4.9+2 # only used in the flutter part #Database sembast: ^2.4.7+6 xxtea: ^2.0.3 - shared_preferences: ^0.5.10 + shared_preferences: ^0.5.10 # only used in the flutter part # Utils - path_provider: ^1.6.14 + path_provider: ^1.6.14 # only used in the flutter part uuid: ^2.2.2 - package_info: ^0.4.3 + package_info: ^0.4.3 # only used in the flutter part meta: ^1.1.8 path: ^1.7.0 diff --git a/test/parse_client_configuration_test.dart b/test/parse_client_configuration_test.dart index 94c8b1e8c..87f9ab35c 100644 --- a/test/parse_client_configuration_test.dart +++ b/test/parse_client_configuration_test.dart @@ -10,8 +10,11 @@ void main() { clientKey: 'clientKey', liveQueryUrl: 'liveQueryUrl', appName: 'appName', + appPackageName: 'somePackageName', + appVersion: 'someAppVersion', masterKey: 'masterKey', sessionId: 'sessionId', + fileDirectory: 'someDirectory', debug: true); expect(ParseCoreData().applicationId, 'appId'); @@ -19,8 +22,11 @@ void main() { expect(ParseCoreData().clientKey, 'clientKey'); expect(ParseCoreData().liveQueryURL, 'liveQueryUrl'); expect(ParseCoreData().appName, 'appName'); + expect(ParseCoreData().appPackageName, 'somePackageName'); + expect(ParseCoreData().appVersion, 'someAppVersion'); expect(ParseCoreData().masterKey, 'masterKey'); expect(ParseCoreData().sessionId, 'sessionId'); expect(ParseCoreData().debug, true); + expect(ParseCoreData().fileDirectory, 'someDirectory'); }); } diff --git a/test/parse_query_test.dart b/test/parse_query_test.dart index 7506f095f..3a5133fbc 100644 --- a/test/parse_query_test.dart +++ b/test/parse_query_test.dart @@ -12,10 +12,22 @@ void main() { test('whereRelatedTo', () async { final MockClient client = MockClient(); - await Parse().initialize('appId', 'https://test.parse.com', debug: true); + await Parse().initialize( + 'appId', + 'https://test.parse.com', + debug: true, + // to prevent automatic detection + fileDirectory: 'someDirectory', + // to prevent automatic detection + appName: 'appName', + // to prevent automatic detection + appPackageName: 'somePackageName', + // to prevent automatic detection + appVersion: 'someAppVersion', + ); final QueryBuilder queryBuilder = - QueryBuilder(ParseObject('_User', client: client)); + QueryBuilder(ParseObject('_User', client: client)); queryBuilder.whereRelatedTo('likes', 'Post', '8TOXdXf3tz'); when(client.data).thenReturn(ParseCoreData()); @@ -27,7 +39,7 @@ void main() { final Uri expectedQuery = Uri( query: - 'where={"\$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}'); + 'where={"\$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}'); expect(result.query, expectedQuery.query); }); });