Showing
7 changed files
with
300 additions
and
6 deletions
| @@ -12,6 +12,9 @@ import '../../../../core/logging/app_logger.dart'; | @@ -12,6 +12,9 @@ import '../../../../core/logging/app_logger.dart'; | ||
| 12 | import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; | 12 | import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 13 | import '../../../../core/services/raw_data_service/health_raw_models.dart'; | 13 | import '../../../../core/services/raw_data_service/health_raw_models.dart'; |
| 14 | import '../../../../core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart'; | 14 | import '../../../../core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart'; |
| 15 | +import '../../../../core/services/raw_data_service/platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart'; | ||
| 16 | +import '../../../../core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart'; | ||
| 17 | +import '../../../../data/models/enums/app_enums.dart'; | ||
| 15 | import '../../../../data/local/user_preferences_storage.dart'; | 18 | import '../../../../data/local/user_preferences_storage.dart'; |
| 16 | import '../../../../l10n/l10n_extensions.dart'; | 19 | import '../../../../l10n/l10n_extensions.dart'; |
| 17 | import '../../../../core/platform/pigeon_api_facade.dart'; | 20 | import '../../../../core/platform/pigeon_api_facade.dart'; |
| @@ -35,6 +38,7 @@ enum DeveloperOptionsAction { | @@ -35,6 +38,7 @@ enum DeveloperOptionsAction { | ||
| 35 | shareFlutterObserverRecord, | 38 | shareFlutterObserverRecord, |
| 36 | shareLocalNotificationDebugRecord, | 39 | shareLocalNotificationDebugRecord, |
| 37 | sendTestHrvLocalNotification, | 40 | sendTestHrvLocalNotification, |
| 41 | + pullOhosRawData, | ||
| 38 | clearHealthRawDataDatabaseAndUploadLog, | 42 | clearHealthRawDataDatabaseAndUploadLog, |
| 39 | globalEnv, | 43 | globalEnv, |
| 40 | } | 44 | } |
| @@ -92,6 +96,10 @@ class DeveloperOptionsController extends GetxController { | @@ -92,6 +96,10 @@ class DeveloperOptionsController extends GetxController { | ||
| 92 | action: DeveloperOptionsAction.sendTestHrvLocalNotification, | 96 | action: DeveloperOptionsAction.sendTestHrvLocalNotification, |
| 93 | ), | 97 | ), |
| 94 | DeveloperOptionsItem( | 98 | DeveloperOptionsItem( |
| 99 | + title: '主动拉取 OHOS 原始数据', | ||
| 100 | + action: DeveloperOptionsAction.pullOhosRawData, | ||
| 101 | + ), | ||
| 102 | + DeveloperOptionsItem( | ||
| 95 | title: '清除本地数据库、上传日志', | 103 | title: '清除本地数据库、上传日志', |
| 96 | action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, | 104 | action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, |
| 97 | ), | 105 | ), |
| @@ -123,6 +131,10 @@ class DeveloperOptionsController extends GetxController { | @@ -123,6 +131,10 @@ class DeveloperOptionsController extends GetxController { | ||
| 123 | action: DeveloperOptionsAction.shareLocalNotificationDebugRecord, | 131 | action: DeveloperOptionsAction.shareLocalNotificationDebugRecord, |
| 124 | ), | 132 | ), |
| 125 | DeveloperOptionsItem( | 133 | DeveloperOptionsItem( |
| 134 | + title: '主动拉取 OHOS 原始数据', | ||
| 135 | + action: DeveloperOptionsAction.pullOhosRawData, | ||
| 136 | + ), | ||
| 137 | + DeveloperOptionsItem( | ||
| 126 | title: '清除本地数据库、上传日志', | 138 | title: '清除本地数据库、上传日志', |
| 127 | action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, | 139 | action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, |
| 128 | ), | 140 | ), |
| @@ -153,6 +165,9 @@ class DeveloperOptionsController extends GetxController { | @@ -153,6 +165,9 @@ class DeveloperOptionsController extends GetxController { | ||
| 153 | case DeveloperOptionsAction.sendTestHrvLocalNotification: | 165 | case DeveloperOptionsAction.sendTestHrvLocalNotification: |
| 154 | await sendTestHrvLocalNotification(); | 166 | await sendTestHrvLocalNotification(); |
| 155 | break; | 167 | break; |
| 168 | + case DeveloperOptionsAction.pullOhosRawData: | ||
| 169 | + await pullOhosRawData(); | ||
| 170 | + break; | ||
| 156 | case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog: | 171 | case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog: |
| 157 | await clearHealthRawDataDatabaseAndUploadLog(); | 172 | await clearHealthRawDataDatabaseAndUploadLog(); |
| 158 | break; | 173 | break; |
| @@ -331,6 +346,64 @@ class DeveloperOptionsController extends GetxController { | @@ -331,6 +346,64 @@ class DeveloperOptionsController extends GetxController { | ||
| 331 | } | 346 | } |
| 332 | } | 347 | } |
| 333 | 348 | ||
| 349 | + Future<void> pullOhosRawData() async { | ||
| 350 | + try { | ||
| 351 | + final userId = | ||
| 352 | + _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0; | ||
| 353 | + if (userId <= 0) { | ||
| 354 | + Get.snackbar('OHOS 拉取失败', '当前用户 id 无效'); | ||
| 355 | + return; | ||
| 356 | + } | ||
| 357 | + final syncService = createDefaultOhosHealthRawDataSyncService( | ||
| 358 | + userIdProvider: () => userId, | ||
| 359 | + ); | ||
| 360 | + final now = DateTime.now(); | ||
| 361 | + final endTime = now.millisecondsSinceEpoch ~/ 1000; | ||
| 362 | + final startTime = | ||
| 363 | + now.subtract(const Duration(days: 7)).millisecondsSinceEpoch ~/ 1000; | ||
| 364 | + final dataTypes = <int>[ | ||
| 365 | + HealthDataUploadType.hrv.type, | ||
| 366 | + HealthDataUploadType.heartRate.type, | ||
| 367 | + HealthDataUploadType.restingHeartRate.type, | ||
| 368 | + OhosHealthRawDataType.sleepAnalysis, | ||
| 369 | + ]; | ||
| 370 | + | ||
| 371 | + await LoadingService.instance.run(() async { | ||
| 372 | + final results = <OhosHealthRawDataSyncResult>[]; | ||
| 373 | + for (final dataType in dataTypes) { | ||
| 374 | + AppLogger.i( | ||
| 375 | + '${OhosHealthRawDataSyncService.logMarker} ' | ||
| 376 | + 'developer_options_pull_start dataType=$dataType ' | ||
| 377 | + 'startTime=$startTime endTime=$endTime', | ||
| 378 | + ); | ||
| 379 | + results.add( | ||
| 380 | + await syncService.syncRawData( | ||
| 381 | + dataType: dataType, | ||
| 382 | + startTime: startTime, | ||
| 383 | + endTime: endTime, | ||
| 384 | + ), | ||
| 385 | + ); | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + final storedCount = results.fold<int>( | ||
| 389 | + 0, | ||
| 390 | + (sum, result) => sum + result.storedCount, | ||
| 391 | + ); | ||
| 392 | + final pageCount = results.fold<int>( | ||
| 393 | + 0, | ||
| 394 | + (sum, result) => sum + result.pageCount, | ||
| 395 | + ); | ||
| 396 | + Get.snackbar( | ||
| 397 | + 'OHOS 原始数据拉取完成', | ||
| 398 | + '类型 ${results.length} 个,页数 $pageCount,写入 $storedCount 条', | ||
| 399 | + ); | ||
| 400 | + }); | ||
| 401 | + } catch (error, stackTrace) { | ||
| 402 | + AppLogger.e('Pull OHOS raw data failed', error, stackTrace); | ||
| 403 | + Get.snackbar('OHOS 拉取失败', error.toString()); | ||
| 404 | + } | ||
| 405 | + } | ||
| 406 | + | ||
| 334 | Future<void> clearHealthRawDataDatabaseAndUploadLog() async { | 407 | Future<void> clearHealthRawDataDatabaseAndUploadLog() async { |
| 335 | try { | 408 | try { |
| 336 | await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog(); | 409 | await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog(); |
| @@ -47,7 +47,7 @@ class HarmonyApi { | @@ -47,7 +47,7 @@ class HarmonyApi { | ||
| 47 | return safeCall( | 47 | return safeCall( |
| 48 | call: () async { | 48 | call: () async { |
| 49 | final queryParameters = <String, dynamic>{ | 49 | final queryParameters = <String, dynamic>{ |
| 50 | - 'date_type': dataType.dataType, | 50 | + 'data_type': dataType.dataType, |
| 51 | 'start_date': startDate, | 51 | 'start_date': startDate, |
| 52 | 'end_date': endDate, | 52 | 'end_date': endDate, |
| 53 | }; | 53 | }; |
| @@ -137,8 +137,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource { | @@ -137,8 +137,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource { | ||
| 137 | class OhosHealthRawDataSource implements HealthRawDataSource { | 137 | class OhosHealthRawDataSource implements HealthRawDataSource { |
| 138 | OhosHealthRawDataSource({ | 138 | OhosHealthRawDataSource({ |
| 139 | OhosHealthRawDataSyncService? syncService, | 139 | OhosHealthRawDataSyncService? syncService, |
| 140 | - }) : _syncService = | ||
| 141 | - syncService ?? createDefaultOhosHealthRawDataSyncService(); | 140 | + int Function()? userIdProvider, |
| 141 | + }) : _syncService = syncService ?? | ||
| 142 | + createDefaultOhosHealthRawDataSyncService( | ||
| 143 | + userIdProvider: userIdProvider, | ||
| 144 | + ); | ||
| 142 | 145 | ||
| 143 | final OhosHealthRawDataSyncService _syncService; | 146 | final OhosHealthRawDataSyncService _syncService; |
| 144 | 147 |
| @@ -5,13 +5,20 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; | @@ -5,13 +5,20 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; | ||
| 5 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 6 | 6 | ||
| 7 | import 'huawei_health_data_type.dart'; | 7 | import 'huawei_health_data_type.dart'; |
| 8 | +import 'ohos_health_raw_data_local_store.dart'; | ||
| 8 | import 'ohos_health_raw_data_sync_service.dart'; | 9 | import 'ohos_health_raw_data_sync_service.dart'; |
| 9 | 10 | ||
| 10 | -OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService() { | 11 | +OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({ |
| 12 | + int Function()? userIdProvider, | ||
| 13 | +}) { | ||
| 14 | + final localStore = OhosHealthRawDataSqliteStore( | ||
| 15 | + userIdProvider: userIdProvider, | ||
| 16 | + ); | ||
| 11 | if (!Get.isRegistered<HarmonyApi>()) { | 17 | if (!Get.isRegistered<HarmonyApi>()) { |
| 12 | - return OhosHealthRawDataSyncService(); | 18 | + return OhosHealthRawDataSyncService(localStore: localStore); |
| 13 | } | 19 | } |
| 14 | return OhosHealthRawDataSyncService( | 20 | return OhosHealthRawDataSyncService( |
| 21 | + localStore: localStore, | ||
| 15 | remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource( | 22 | remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource( |
| 16 | HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()), | 23 | HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()), |
| 17 | ), | 24 | ), |
| @@ -28,7 +28,10 @@ class OHOSHealthRawDataCoreService { | @@ -28,7 +28,10 @@ class OHOSHealthRawDataCoreService { | ||
| 28 | int Function()? userIdProvider, | 28 | int Function()? userIdProvider, |
| 29 | bool uploadResultsAfterCalculation = true, | 29 | bool uploadResultsAfterCalculation = true, |
| 30 | HealthApi? serverHealthApi, | 30 | HealthApi? serverHealthApi, |
| 31 | - }) : _rawDataSource = rawDataSource ?? OhosHealthRawDataSource(), | 31 | + }) : _rawDataSource = rawDataSource ?? |
| 32 | + OhosHealthRawDataSource( | ||
| 33 | + userIdProvider: userIdProvider, | ||
| 34 | + ), | ||
| 32 | _localStore = localStore ?? | 35 | _localStore = localStore ?? |
| 33 | HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'), | 36 | HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'), |
| 34 | _environmentConfig = environmentConfig, | 37 | _environmentConfig = environmentConfig, |
| 1 | +import 'dart:convert'; | ||
| 2 | +import 'dart:io'; | ||
| 3 | + | ||
| 4 | +import 'package:path_provider/path_provider.dart'; | ||
| 5 | +import 'package:sqflite/sqflite.dart'; | ||
| 6 | + | ||
| 7 | +import 'huawei_health_data_type.dart'; | ||
| 8 | +import 'ohos_health_raw_data_sync_service.dart'; | ||
| 9 | + | ||
| 10 | +class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { | ||
| 11 | + OhosHealthRawDataSqliteStore({ | ||
| 12 | + required int Function()? userIdProvider, | ||
| 13 | + Directory? rootDirectory, | ||
| 14 | + DatabaseFactory? databaseFactory, | ||
| 15 | + DateTime Function()? nowProvider, | ||
| 16 | + }) : _userIdProvider = userIdProvider, | ||
| 17 | + _rootDirectory = rootDirectory, | ||
| 18 | + _databaseFactory = databaseFactory, | ||
| 19 | + _nowProvider = nowProvider ?? DateTime.now; | ||
| 20 | + | ||
| 21 | + static const rawDataTable = 'ohos_raw_data'; | ||
| 22 | + static const sleepDataTable = 'ohos_sleep_data'; | ||
| 23 | + | ||
| 24 | + final int Function()? _userIdProvider; | ||
| 25 | + final Directory? _rootDirectory; | ||
| 26 | + final DatabaseFactory? _databaseFactory; | ||
| 27 | + final DateTime Function() _nowProvider; | ||
| 28 | + final Map<int, Database> _opened = <int, Database>{}; | ||
| 29 | + | ||
| 30 | + int get _userId { | ||
| 31 | + final userId = _userIdProvider?.call() ?? 0; | ||
| 32 | + if (userId <= 0) { | ||
| 33 | + throw StateError('OhosHealthRawDataSqliteStore requires a valid userId'); | ||
| 34 | + } | ||
| 35 | + return userId; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + Future<String> dbPath(int userId) async { | ||
| 39 | + final dir = _rootDirectory ?? await getApplicationDocumentsDirectory(); | ||
| 40 | + return '${dir.path}/ohos_raw_data_$userId.sqlite'; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @override | ||
| 44 | + Future<int?> latestDataTime({required int dataType}) async { | ||
| 45 | + final db = await _database(_userId); | ||
| 46 | + if (dataType == OhosHealthRawDataType.sleepAnalysis) { | ||
| 47 | + final rows = await db.query( | ||
| 48 | + sleepDataTable, | ||
| 49 | + columns: ['to_time'], | ||
| 50 | + orderBy: 'to_time DESC', | ||
| 51 | + limit: 1, | ||
| 52 | + ); | ||
| 53 | + return rows.isEmpty ? null : rows.first['to_time'] as int; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + final storedDataType = _storedHealthDataType(dataType); | ||
| 57 | + final rows = await db.query( | ||
| 58 | + rawDataTable, | ||
| 59 | + columns: ['data_time'], | ||
| 60 | + where: 'data_type = ?', | ||
| 61 | + whereArgs: [storedDataType], | ||
| 62 | + orderBy: 'data_time DESC', | ||
| 63 | + limit: 1, | ||
| 64 | + ); | ||
| 65 | + return rows.isEmpty ? null : rows.first['data_time'] as int; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + @override | ||
| 69 | + Future<void> upsertRawDataBatch({ | ||
| 70 | + required int dataType, | ||
| 71 | + required List<OhosHealthRawDataItem> items, | ||
| 72 | + }) async { | ||
| 73 | + if (items.isEmpty) return; | ||
| 74 | + final db = await _database(_userId); | ||
| 75 | + final createTime = _nowProvider().millisecondsSinceEpoch ~/ 1000; | ||
| 76 | + await db.transaction((txn) async { | ||
| 77 | + if (dataType == OhosHealthRawDataType.sleepAnalysis) { | ||
| 78 | + for (final item in items) { | ||
| 79 | + await txn.insert( | ||
| 80 | + sleepDataTable, | ||
| 81 | + _sleepRow(item, createTime), | ||
| 82 | + conflictAlgorithm: ConflictAlgorithm.ignore, | ||
| 83 | + ); | ||
| 84 | + } | ||
| 85 | + return; | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + for (final item in items) { | ||
| 89 | + await txn.insert( | ||
| 90 | + rawDataTable, | ||
| 91 | + _rawRow(item, createTime), | ||
| 92 | + conflictAlgorithm: ConflictAlgorithm.ignore, | ||
| 93 | + ); | ||
| 94 | + } | ||
| 95 | + }); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + Future<void> close({int? userId}) async { | ||
| 99 | + if (userId != null) { | ||
| 100 | + final db = _opened.remove(userId); | ||
| 101 | + if (db != null) { | ||
| 102 | + await db.close(); | ||
| 103 | + } | ||
| 104 | + return; | ||
| 105 | + } | ||
| 106 | + final databases = _opened.values.toList(); | ||
| 107 | + _opened.clear(); | ||
| 108 | + for (final db in databases) { | ||
| 109 | + await db.close(); | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + Future<Database> _database(int userId) async { | ||
| 114 | + final existing = _opened[userId]; | ||
| 115 | + if (existing != null && existing.isOpen) return existing; | ||
| 116 | + final path = await dbPath(userId); | ||
| 117 | + final parent = File(path).parent; | ||
| 118 | + if (!await parent.exists()) { | ||
| 119 | + await parent.create(recursive: true); | ||
| 120 | + } | ||
| 121 | + final factory = _databaseFactory ?? databaseFactory; | ||
| 122 | + final db = await factory.openDatabase( | ||
| 123 | + path, | ||
| 124 | + options: OpenDatabaseOptions( | ||
| 125 | + version: 1, | ||
| 126 | + onCreate: (db, version) => _createTables(db), | ||
| 127 | + ), | ||
| 128 | + ); | ||
| 129 | + _opened[userId] = db; | ||
| 130 | + return db; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + Future<void> _createTables(Database db) async { | ||
| 134 | + await db.execute(''' | ||
| 135 | +CREATE TABLE IF NOT EXISTS $rawDataTable ( | ||
| 136 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 137 | + data_type INTEGER NOT NULL, | ||
| 138 | + data_time INTEGER NOT NULL, | ||
| 139 | + payload TEXT NOT NULL, | ||
| 140 | + create_time INTEGER NOT NULL, | ||
| 141 | + UNIQUE (data_type, data_time, payload) | ||
| 142 | +) | ||
| 143 | +'''); | ||
| 144 | + await db.execute(''' | ||
| 145 | +CREATE TABLE IF NOT EXISTS $sleepDataTable ( | ||
| 146 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 147 | + data_type INTEGER NOT NULL, | ||
| 148 | + from_time INTEGER NOT NULL, | ||
| 149 | + to_time INTEGER NOT NULL, | ||
| 150 | + payload TEXT NOT NULL, | ||
| 151 | + create_time INTEGER NOT NULL, | ||
| 152 | + UNIQUE (data_type, from_time, to_time, payload) | ||
| 153 | +) | ||
| 154 | +'''); | ||
| 155 | + await db.execute( | ||
| 156 | + 'CREATE INDEX IF NOT EXISTS idx_ohos_raw_data_time ' | ||
| 157 | + 'ON $rawDataTable(data_type, data_time)', | ||
| 158 | + ); | ||
| 159 | + await db.execute( | ||
| 160 | + 'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time ' | ||
| 161 | + 'ON $sleepDataTable(to_time)', | ||
| 162 | + ); | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + Map<String, Object?> _rawRow( | ||
| 166 | + OhosHealthRawDataItem item, | ||
| 167 | + int createTime, | ||
| 168 | + ) { | ||
| 169 | + return { | ||
| 170 | + 'data_type': item.dataType, | ||
| 171 | + 'data_time': item.dataTime, | ||
| 172 | + 'payload': jsonEncode(item.payload), | ||
| 173 | + 'create_time': createTime, | ||
| 174 | + }; | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + Map<String, Object?> _sleepRow( | ||
| 178 | + OhosHealthRawDataItem item, | ||
| 179 | + int createTime, | ||
| 180 | + ) { | ||
| 181 | + return { | ||
| 182 | + 'data_type': item.dataType, | ||
| 183 | + 'from_time': _numPayload(item.payload, 'from_time') ?? 0, | ||
| 184 | + 'to_time': _numPayload(item.payload, 'to_time') ?? item.dataTime, | ||
| 185 | + 'payload': jsonEncode(item.payload), | ||
| 186 | + 'create_time': createTime, | ||
| 187 | + }; | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + int _storedHealthDataType(int dataType) { | ||
| 191 | + if (dataType == OhosHealthRawDataType.activitySummary) { | ||
| 192 | + return HuaweiHealthDataType.activity.dataType; | ||
| 193 | + } | ||
| 194 | + if (dataType == OhosHealthRawDataType.workout) { | ||
| 195 | + return HuaweiHealthDataType.exerciseDuration.dataType; | ||
| 196 | + } | ||
| 197 | + return dataType; | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + int? _numPayload(Map<String, Object?> payload, String key) { | ||
| 201 | + final value = payload[key]; | ||
| 202 | + return value is num ? value.toInt() : null; | ||
| 203 | + } | ||
| 204 | +} |
| @@ -211,6 +211,10 @@ abstract class PlatformHostApi { | @@ -211,6 +211,10 @@ abstract class PlatformHostApi { | ||
| 211 | @async | 211 | @async |
| 212 | bool performRestore(); | 212 | bool performRestore(); |
| 213 | 213 | ||
| 214 | + /// 获取应用 Documents 目录路径。 | ||
| 215 | + // @async | ||
| 216 | + // String getApplicationDocumentsPath(); | ||
| 217 | + | ||
| 214 | /// 上传文件到云端 | 218 | /// 上传文件到云端 |
| 215 | /// 注意catch flutter error | 219 | /// 注意catch flutter error |
| 216 | @async | 220 | @async |
-
Please register or login to post a comment