如何在Flutter中以防御式编程方式从Firestore请求数据
核心实现思路
所有Firestore数据读写环节都增加类型校验层,解析异常时返回兜底值保证业务流程不中断,同时收集异常信息上报。
具体实现方案
1. 封装通用类型安全解析工具
避免直接对Firestore返回的字段做强转,通过通用方法处理类型转换异常,同步记录错误信息:
class FirestoreSafeParser { static T? _parse<T>(dynamic rawValue, T Function(dynamic) converter, {T? defaultValue}) { try { if (rawValue == null) return defaultValue; return converter(rawValue); } catch (e, stackTrace) { FirestoreDamageReporter.collectError( error: e, stackTrace: stackTrace, rawValue: rawValue, expectType: T.toString(), ); return defaultValue; } } // 基础类型解析方法,按需扩展 static int parseInt(dynamic rawValue, {int defaultValue = 0}) { return _parse<int>(rawValue, (v) => v is int ? v : num.parse(v.toString()).toInt(), defaultValue: defaultValue)!; } static String parseString(dynamic rawValue, {String defaultValue = ''}) { return _parse<String>(rawValue, (v) => v.toString(), defaultValue: defaultValue)!; } static bool parseBool(dynamic rawValue, {bool defaultValue = false}) { return _parse<bool>(rawValue, (v) => v is bool ? v : v.toString().toLowerCase() == 'true', defaultValue: defaultValue)!; } static DateTime parseDateTime(dynamic rawValue, {DateTime? defaultValue}) { return _parse<DateTime>(rawValue, (v) => v is Timestamp ? v.toDate() : DateTime.parse(v.toString()), defaultValue: defaultValue ?? DateTime.now())!; } }
2. 数据模型层统一使用解析工具
所有Firestore实体类的序列化逻辑全部替换为安全解析方法:
class Product { final String id; final int price; final String title; final DateTime onSaleTime; Product({required this.id, required this.price, required this.title, required this.onSaleTime}); factory Product.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map<String, dynamic>? ?? {}; // 上报时补充文档路径,方便快速定位问题数据 FirestoreDamageReporter.currentRequestPath = doc.reference.path; return Product( id: doc.id, price: FirestoreSafeParser.parseInt(data['price']), title: FirestoreSafeParser.parseString(data['title']), onSaleTime: FirestoreSafeParser.parseDateTime(data['on_sale_time']), ); } }
3. 实现损坏数据上报逻辑
单独封装上报类,收集足够的定位信息后批量上报,减少不必要的接口请求:
class FirestoreDamageReporter { static String? currentRequestPath; static final List<Map<String, dynamic>> _errorQueue = []; static const int _batchReportCount = 5; static void collectError({ required Object error, required StackTrace stackTrace, required dynamic rawValue, required String expectType, }) { _errorQueue.add({ 'doc_path': currentRequestPath, 'error_msg': error.toString(), 'stack_trace': stackTrace.toString(), 'bad_value': rawValue?.toString(), 'expect_type': expectType, 'app_version': '你的当前应用版本号', 'user_uid': '当前登录用户uid(可选)', 'timestamp': DateTime.now().toIso8601String(), }); if (_errorQueue.length >= _batchReportCount) { _flushReport(); } } static Future<void> _flushReport() async { if (_errorQueue.isEmpty) return; // 此处实现上报逻辑,可上报到Firebase Crashlytics、自有日志服务等 // 上报完成清空队列 _errorQueue.clear(); } // 应用退到后台时主动调用,上报残留错误 static Future<void> reportRemaining() => _flushReport(); }
4. 从源头减少坏数据产生
所有写入Firestore的操作前增加校验,拦截不符合格式要求的数据:
extension ProductToFirestore on Product { Map<String, dynamic> toFirestore() { // 开发环境下直接抛出异常,提前发现问题 assert(price >= 0, '商品价格不能为负'); assert(title.isNotEmpty, '商品标题不能为空'); return { 'price': price, 'title': title, 'on_sale_time': Timestamp.fromDate(onSaleTime), // 增加schema版本标记,处理跨版本兼容 'schema_version': 1, }; } }
5. 处理版本兼容问题
读取文档时先判断schema版本,如果文档版本高于当前应用支持的最高版本,走降级逻辑:
factory Product.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map<String, dynamic>? ?? {}; final schemaVersion = FirestoreSafeParser.parseInt(data['schema_version'], defaultValue: 0); // 当前应用最高支持v1版本结构,高于v1的版本提示用户更新 if (schemaVersion > 1) { // 可返回缓存的旧版本数据,或者提示用户更新应用 throw UnsupportedError('请更新应用到最新版本查看该内容'); } // 其余解析逻辑不变 }
内容的提问来源于stack exchange,提问作者Vega180
相关产品推荐
相关产品推荐

