Flutter自定义过滤实现及相关开发问题咨询
Flutter - 自定义过滤的最佳实现方式
当前实现
站点列表页面
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../data/metro_repository.dart'; import '../data/http_metro_datasource.dart'; import '../data/sqflite_metro_datasource.dart'; import '../models/station.dart'; import 'station_detail_page.dart'; class ListaPage extends StatefulWidget { const ListaPage({super.key}); @override State<ListaPage> createState() => _ListaPageState(); } class _ListaPageState extends State<ListaPage> { String _pesquisaNome = ""; @override Widget build(BuildContext context) { final repositorio = MetroRepository( local: context.read<SqfliteMetroDataSource>(), remote: context.read<HttpMetroDataSource>(), ); return Scaffold( appBar: AppBar( title: const Text("Stations"), ), body: FutureBuilder<List<Station>>( future: repositorio.getAllStations(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(), ); } if (snapshot.hasError) { return const Center( child: Text("Error loading stations"), ); } final stations = snapshot.data ?? []; final filteredStations = stations.where((station) { final name = station.name.toLowerCase().trim(); final matchesSearch = name.contains(_pesquisaNome.toLowerCase().trim()); final hasLetterL = name.contains("l"); final isSingleWord = name.split(RegExp(r'\s+')).length == 1; final isForbidden = hasLetterL && isSingleWord; return matchesSearch && !isForbidden; }).toList(); return Column( children: [ Padding( padding: const EdgeInsets.all(12), child: TextField( decoration: InputDecoration( hintText: "Search station...", prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), onChanged: (value) { setState(() { _pesquisaNome = value; }); }, ), ), Expanded( child: ListView.builder( itemCount: filteredStations.length, itemBuilder: (context, index) { final station = filteredStations[index]; return Card( child: ListTile( title: Text(station.name), subtitle: Text( station.lineName.toString(), ), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => StationDetailPage( station: station, ), ), ); }, ), ); }, ), ), ], ); }, ), ); } }
仪表盘页面
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../data/metro_repository.dart'; import '../data/http_metro_datasource.dart'; import '../data/sqflite_metro_datasource.dart'; import '../models/station.dart'; class DashboardPage extends StatelessWidget { const DashboardPage({super.key}); @override Widget build(BuildContext context) { final repositorio = MetroRepository( local: context.read<SqfliteMetroDataSource>(), remote: context.read<HttpMetroDataSource>(), ); return Scaffold( appBar: AppBar( title: const Text("Dashboard"), ), body: FutureBuilder<List<Station>>( future: repositorio.getAllStations(), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center( child: CircularProgressIndicator(), ); } final stations = snapshot.data!; final stationsWithA = stations.where((station) { return station.name .toLowerCase() .startsWith("a"); }).length; final filteredStations = stations.where((station) { final name = station.name.toLowerCase().trim(); final hasLetterL = name.contains("l"); final isSingleWord = name.split(RegExp(r'\s+')).length == 1; return hasLetterL && isSingleWord; }).length; return ListView( padding: const EdgeInsets.all(16), children: [ Card( child: ListTile( title: const Text("Stations with A"), trailing: Text( stationsWithA.toString(), ), ), ), Card( child: ListTile( title: const Text( "Filtered Stations", ), trailing: Text( filteredStations.toString(), ), ), ), Card( child: ListTile( title: const Text("Total Stations"), trailing: Text( stations.length.toString(), ), ), ), Card( child: ListTile( title: const Text("Incidents"), trailing: const Text("0"), ), ), ], ); }, ), ); } }
事件校验
String? validateIncident({ required int rating, required String note, }) { if (rating == 5 && note.trim().length < 20) { return "A note with at least 20 characters is required."; } return null; }
技术问询
- 在FutureBuilder中直接执行此类过滤与统计计算是否合理,还是应将该逻辑迁移至repository/service层?
- 在大型Flutter项目中,应如何组织这类业务规则?
- 有没有更简洁的方案来计算仪表盘统计数据,避免每次重建时重复计算?
问题解答
1. FutureBuilder内执行过滤统计的合理性判断
页面专属的临时简单过滤(比如列表的搜索框匹配)可以留在页面,但要封装成单独方法,避免代码堆在builder里。但像你代码中跨页面复用的规则(比如两次用到的「含字母L且为单单词」的过滤逻辑),必须迁移到repository或专门的业务逻辑层:
- 避免重复代码,后续修改只需维护一处;
- 业务逻辑集中管理,UI层仅负责展示,职责边界更清晰;
- 数据量大时,在repo层提前缓存过滤/统计结果,减少UI重建时的重复计算损耗。
2. 大型项目中业务规则的组织方式
- 分层拆分:把业务逻辑抽离到
domain层,比如创建StationFilter、StationStatsCalculator、IncidentValidator这类单一职责的类,repository层负责数据获取并调用domain层的规则处理,UI层只做触发操作和展示; - 复用优先:所有跨页面/组件的规则必须封装成公共类,放在业务相关的公共目录下,禁止在多个页面重复编写相同逻辑;
- 配合状态管理:如果是需要实时更新的过滤/统计,结合Provider、Riverpod等状态管理工具,把计算好的数据存在状态中,UI直接监听状态变化,无需每次重建都重新计算。
3. 避免仪表盘重复计算的方案
- 缓存计算结果:将Dashboard改为StatefulWidget,在首次获取到站点数据时计算统计值并存在state变量中,后续UI重建直接使用缓存值;
- 状态管理托管:创建
StationStatsNotifier这类状态类,在获取到所有站点后一次性计算好stationsWithA、filteredStationsCount等统计值,UI直接消费这些预计算的结果,除非数据源更新,否则不会重新计算; - 缓存数据源:如果站点数据不会频繁更新,把获取到的站点列表存在全局缓存或本地存储中,下次进入页面直接使用缓存数据,跳过重复请求和计算。
内容的提问来源于stack exchange,提问作者Gjk
相关产品推荐
相关产品推荐

