如何在Flutter中实现带科目、时段高级筛选的家教搜索栏?
实现带高级筛选的Flutter家教搜索栏
整体布局思路
用AppBar承载搜索栏和筛选入口,点击筛选图标弹出底部弹窗(showModalBottomSheet)放置所有筛选组件,完成筛选后点击确认按钮应用条件。
核心实现步骤
1. 定义筛选状态变量
在StatefulWidget的State类里维护所有筛选条件的状态:
// 科目选项与选中列表 List<String> subjects = ['数学', '生物', '英语', '物理', '化学']; List<String> selectedSubjects = []; // 日期选项与选中状态(对应每个日期的选中与否) List<String> weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; List<bool> selectedWeekdays = List.filled(7, false); // 时段选项与选中列表 List<String> timeSlots = ['9-11点', '11-13点', '14-16点', '17-19点', '19-21点']; List<String> selectedTimeSlots = []; // 授课时长(默认1小时,范围1-4小时) double classDuration = 1.0;
2. 搜索栏+筛选入口布局
在AppBar中实现基础搜索与筛选触发:
AppBar( title: TextField( decoration: InputDecoration( hintText: '搜索家教', border: InputBorder.none, ), onChanged: (value) { // 此处可添加实时搜索逻辑 }, ), actions: [ IconButton( icon: const Icon(Icons.filter_alt), onPressed: () { // 弹出筛选弹窗 showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => FilterBottomSheet( // 传递状态与回调 subjects: subjects, selectedSubjects: selectedSubjects, onSubjectToggle: (subject) { setState(() { selectedSubjects.contains(subject) ? selectedSubjects.remove(subject) : selectedSubjects.add(subject); }); }, weekdays: weekdays, selectedWeekdays: selectedWeekdays, onWeekdayToggle: (index) { setState(() { selectedWeekdays[index] = !selectedWeekdays[index]; }); }, timeSlots: timeSlots, selectedTimeSlots: selectedTimeSlots, onTimeSlotToggle: (slot) { setState(() { selectedTimeSlots.contains(slot) ? selectedTimeSlots.remove(slot) : selectedTimeSlots.add(slot); }); }, classDuration: classDuration, onDurationChanged: (value) { setState(() { classDuration = value; }); }, onConfirm: () { // 应用筛选条件,执行搜索 Navigator.pop(context); // 此处可替换为实际的搜索接口调用 print('筛选条件:科目=$selectedSubjects,日期=$selectedWeekdays,时段=$selectedTimeSlots,时长=$classDuration'); }, ), ); }, ), ], )
3. 筛选弹窗组件(FilterBottomSheet)
单独封装成可复用组件,接收状态与回调:
class FilterBottomSheet extends StatelessWidget { final List<String> subjects; final List<String> selectedSubjects; final Function(String) onSubjectToggle; final List<String> weekdays; final List<bool> selectedWeekdays; final Function(int) onWeekdayToggle; final List<String> timeSlots; final List<String> selectedTimeSlots; final Function(String) onTimeSlotToggle; final double classDuration; final Function(double) onDurationChanged; final VoidCallback onConfirm; const FilterBottomSheet({ super.key, required this.subjects, required this.selectedSubjects, required this.onSubjectToggle, required this.weekdays, required this.selectedWeekdays, required this.onWeekdayToggle, required this.timeSlots, required this.selectedTimeSlots, required this.onTimeSlotToggle, required this.classDuration, required this.onDurationChanged, required this.onConfirm, }); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, left: 16, right: 16, top: 16, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 科目筛选 const Text('选择科目', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: subjects.map((subject) { return Chip( label: Text(subject), backgroundColor: selectedSubjects.contains(subject) ? Colors.blue : Colors.grey[200], labelColor: selectedSubjects.contains(subject) ? Colors.white : Colors.black, onPressed: () => onSubjectToggle(subject), ); }).toList(), ), const SizedBox(height: 16), // 日期筛选 const Text('选择授课日期', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), ToggleButtons( isSelected: selectedWeekdays, onPressed: onWeekdayToggle, children: weekdays.map((day) => Text(day)).toList(), selectedColor: Colors.white, fillColor: Colors.blue, borderColor: Colors.grey, ), const SizedBox(height: 16), // 时段筛选 const Text('选择授课时段', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: timeSlots.map((slot) { return Chip( label: Text(slot), backgroundColor: selectedTimeSlots.contains(slot) ? Colors.blue : Colors.grey[200], labelColor: selectedTimeSlots.contains(slot) ? Colors.white : Colors.black, onPressed: () => onTimeSlotToggle(slot), ); }).toList(), ), const SizedBox(height: 16), // 时长滑块 Text('授课时长:${classDuration.toStringAsFixed(1)}小时', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Slider( value: classDuration, min: 1.0, max: 4.0, divisions: 6, // 1-4小时,每0.5小时一个刻度 onChanged: onDurationChanged, label: '${classDuration.toStringAsFixed(1)}小时', ), const SizedBox(height: 24), // 确认按钮 SizedBox( width: double.infinity, child: ElevatedButton( onPressed: onConfirm, child: const Text('应用筛选'), ), ), const SizedBox(height: 16), ], ), ), ); } }
关键逻辑说明
- 所有筛选状态通过父组件的
setState更新,确保UI实时响应 - 用
Wrap+Chip实现标签式多选,选中状态通过背景色区分,样式易调整 ToggleButtons适合日期这类固定数量的选项,布局更规整Slider通过divisions设置刻度,让用户精准选择时长- 弹窗设置
isScrollControlled: true,避免键盘遮挡(若后续添加输入类筛选)
后续扩展建议
- 将每个筛选模块拆分为独立小组件,提升代码可读性
- 添加“重置筛选”按钮,一键清空所有选择
- 对接后端API,将筛选条件作为请求参数发起搜索
内容的提问来源于stack exchange,提问作者Aidir
相关产品推荐
相关产品推荐

