Flutter Firestore多条件过滤数据遇索引报错及功能需求实现
Flutter Firestore 过滤数据问题解决
问题概述
在Flutter项目中使用Firestore的transaction集合,需要按transactionType和date字段过滤数据并展示:
- 已实现
transactionType单字段过滤,但日期过滤存在逻辑错误 - 同时选择两类过滤时触发
FAILED_PRECONDITION错误,提示需创建复合索引 - 需要实现未选择任何过滤时展示全量数据
解决方案
1. 复合索引创建
Firestore对多字段组合查询要求创建复合索引,按以下配置在Firebase控制台手动创建:
- 目标集合:
transaction - 索引字段:
transactionType(升序)、date(升序) - 注意:索引创建需几分钟生效,期间查询可能仍报错
2. 日期过滤逻辑修正
原代码中日期Radio的value与groupValue类型不匹配,且日期判断逻辑错误(比如"今日"应该查询大于等于当日0点的数据,而非小于昨日时间),修正点:
- 将
selectedDateFilter改为存储日期范围标记(如'today'、'yesterday'、'weekAgo'),避免DateTime对象对比的精度问题 - 按标记生成正确时间范围:
- 今日:
date >= 当前日期的0点 - 昨日:
date >= 昨日0点 且 date < 当前日期0点 - 一周前:
date >= 7天前的0点
- 今日:
3. 全量数据展示逻辑
原代码中当selectedTransactionType为空且selectedDateRange为null时,filteredQuery直接指向原始集合,已支持全量展示,只需确保过滤条件清空时状态正确即可。
修正后完整代码
import "package:flutter/material.dart"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:intl/intl.dart"; // 扩展方法:获取当日起始时间(00:00:00) extension DateTimeExtension on DateTime { DateTime get startOfDay => DateTime(year, month, day); } class FilteringScreen extends StatefulWidget { @override _FilteringScreenState createState() => _FilteringScreenState(); } class _FilteringScreenState extends State<FilteringScreen> { String selectedTransactionType = ''; String? selectedDateRange; // 存储日期范围标记:'today'/'yesterday'/'weekAgo' @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('过滤页面'), actions: [ IconButton( icon: const Icon(Icons.filter_alt), onPressed: () async { await _showFilterDialog(context); }, ), ], ), body: _buildStreamBuilder(), ); } StreamBuilder<QuerySnapshot> _buildStreamBuilder() { CollectionReference transactions = FirebaseFirestore.instance.collection('transaction'); Query filteredQuery = transactions; // 交易类型过滤 if (selectedTransactionType.isNotEmpty) { filteredQuery = filteredQuery.where('transactionType', isEqualTo: selectedTransactionType); } // 日期范围过滤 if (selectedDateRange != null) { DateTime now = DateTime.now(); switch (selectedDateRange) { case 'today': filteredQuery = filteredQuery.where('date', isGreaterThanOrEqualTo: now.startOfDay); break; case 'yesterday': DateTime yesterdayStart = now.subtract(const Duration(days: 1)).startOfDay; filteredQuery = filteredQuery .where('date', isGreaterThanOrEqualTo: yesterdayStart) .where('date', isLessThan: now.startOfDay); break; case 'weekAgo': DateTime weekAgoStart = now.subtract(const Duration(days: 7)).startOfDay; filteredQuery = filteredQuery.where('date', isGreaterThanOrEqualTo: weekAgoStart); break; } } return StreamBuilder<QuerySnapshot>( stream: filteredQuery.snapshots(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { return const Center(child: Text('暂无数据')); } var docs = snapshot.data!.docs; return ListView.builder( itemCount: docs.length, itemBuilder: (context, index) { var data = docs[index]; DateTime transactionDate = (data['date'] as Timestamp).toDate(); return ListTile( title: Text(data['imei']), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('类型: ${data['transactionType']}'), Text('日期: ${DateFormat('yyyy-MM-dd HH:mm').format(transactionDate)}'), ], ), ); }, ); }, ); } Future<void> _showFilterDialog(BuildContext context) async { await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('过滤选项'), content: SizedBox( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, children: [ // 交易类型选择 const Text('交易类型'), Row( children: [ Radio( value: 'In', groupValue: selectedTransactionType, onChanged: (value) { setState(() { selectedTransactionType = value.toString(); }); }, ), const Text('入库'), Radio( value: 'Out', groupValue: selectedTransactionType, onChanged: (value) { setState(() { selectedTransactionType = value.toString(); }); }, ), const Text('出库'), Radio( value: '', groupValue: selectedTransactionType, onChanged: (value) { setState(() { selectedTransactionType = value.toString(); }); }, ), const Text('全部'), ], ), const SizedBox(height: 16), // 日期范围选择 const Text('日期范围'), Column( children: [ RadioListTile( title: const Text('今日'), value: 'today', groupValue: selectedDateRange, onChanged: (value) { setState(() { selectedDateRange = value; }); }, ), RadioListTile( title: const Text('昨日'), value: 'yesterday', groupValue: selectedDateRange, onChanged: (value) { setState(() { selectedDateRange = value; }); }, ), RadioListTile( title: const Text('一周前至今'), value: 'weekAgo', groupValue: selectedDateRange, onChanged: (value) { setState(() { selectedDateRange = value; }); }, ), RadioListTile( title: const Text('全部日期'), value: null, groupValue: selectedDateRange, onChanged: (value) { setState(() { selectedDateRange = value; }); }, ), ], ), const SizedBox(height: 16), ElevatedButton( onPressed: () { Navigator.of(context).pop(); setState(() {}); // 触发界面刷新 }, child: const Text('确认'), ), ], ), ), ); }, ); } }
补充说明
- 新增
DateTimeExtension扩展方法,简化当日起始时间的获取 - 将日期选择改为
RadioListTile,提升UI可读性和交互体验 - 修复日期查询逻辑错误,确保过滤结果符合预期
- 增加日期格式化展示,让列表项信息更完整
内容的提问来源于stack exchange,提问作者Anees Dahot
相关产品推荐
相关产品推荐

