Flutter for OpenHarmony 美食烹饪助手 App 实战:菜谱搜索功能实现
本文介绍了Flutter菜谱应用的搜索功能实现方案。采用实时搜索方式,用户在输入关键词时立即显示结果。界面设计分为未输入状态(显示热门搜索标签)和已输入状态(展示搜索结果)。使用StatefulWidget管理搜索状态,通过TextField控制器处理输入内容,结合防抖技术优化搜索性能。热门标签采用Chip组件实现点击搜索,搜索结果使用ListView展示。该方案实现了流畅的用户搜索体验,减少了不

在海量的菜谱中找到想要的那一道,搜索是最直接的方式。今天我们要实现菜谱搜索功能,让用户能够通过关键词快速找到目标菜谱。
搜索功能的设计思路
搜索功能要解决的核心问题是:如何让用户快速准确地找到想要的菜谱?我选择了实时搜索的方式,用户输入关键词时立即显示结果,不需要点击搜索按钮。
搜索页面分为两个状态:未输入和已输入。未输入时显示热门搜索标签,帮助用户快速开始搜索。已输入时显示搜索结果,按相关度排序。
热门搜索标签使用 Chip 组件,点击标签就自动填入搜索框。这种设计在电商应用中很常见,能有效引导用户搜索。
创建有状态组件
搜索功能需要管理输入状态,所以要使用 StatefulWidget。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class RecipeSearchPage extends StatefulWidget {
const RecipeSearchPage({super.key});
State<RecipeSearchPage> createState() => _RecipeSearchPageState();
}
class _RecipeSearchPageState extends State<RecipeSearchPage> {
final TextEditingController _controller = TextEditingController();
TextEditingController 用于控制搜索框的输入。我们可以通过它获取输入内容,也可以程序化地设置输入内容。
创建 controller 后要记得在 dispose 方法中释放,避免内存泄漏:
void dispose() {
_controller.dispose();
super.dispose();
}
构建搜索界面
搜索框放在 AppBar 的 title 位置,这是搜索页面的标准设计。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: '搜索菜谱...',
border: InputBorder.none,
hintStyle: TextStyle(color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
),
TextField 使用白色文字,提示文字使用半透明的白色。border 设置为 none,去掉默认的下划线,让搜索框和 AppBar 融为一体。
autofocus 可以设置为 true,让页面打开时自动聚焦搜索框,用户可以直接输入。但这会自动弹出键盘,可能不是所有用户都喜欢。
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
],
),
AppBar 右侧放了一个搜索图标,虽然我们使用实时搜索,但这个图标能让用户知道这是搜索页面。点击图标可以触发搜索,或者关闭键盘。
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Text('热门搜索', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: ['宫保鸡丁', '红烧肉', '糖醋排骨', '麻婆豆腐', '鱼香肉丝']
.map((tag) => Chip(label: Text(tag)))
.toList(),
),
],
),
);
}
}
body 显示热门搜索标签。标题使用粗体,字号 16.sp。标签使用 Wrap 组件排列,会自动换行。
spacing 和 runSpacing 都设置为 8,让标签之间有适当的间距。每个标签使用 Chip 组件,这是 Flutter 提供的标签组件。
实现标签点击
点击热门搜索标签应该自动填入搜索框并触发搜索:
Chip(
label: Text(tag),
onPressed: () {
_controller.text = tag;
_performSearch(tag);
},
)
但 Chip 组件没有 onPressed 属性,需要用 ActionChip 或 GestureDetector 包裹:
GestureDetector(
onTap: () {
_controller.text = tag;
_performSearch(tag);
},
child: Chip(label: Text(tag)),
)
点击标签时,设置 controller 的 text 属性,搜索框会自动显示这个文字。然后调用 _performSearch 方法执行搜索。
实现实时搜索
用户输入时应该实时显示搜索结果:
TextField(
controller: _controller,
onChanged: (value) {
setState(() {
_performSearch(value);
});
},
// ...
)
onChanged 回调在输入内容改变时触发。我们调用 setState 触发重建,并执行搜索。
但这样会导致每输入一个字符就搜索一次,如果搜索涉及网络请求,会造成大量无效请求。可以使用防抖技术,延迟一段时间再搜索:
Timer? _debounce;
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
_performSearch(value);
});
}
void dispose() {
_debounce?.cancel();
_controller.dispose();
super.dispose();
}
使用 Timer 延迟 500 毫秒执行搜索。如果在这期间用户又输入了新的字符,就取消之前的 Timer,重新计时。
这样只有用户停止输入 500 毫秒后才会执行搜索,大大减少了搜索次数。
显示搜索结果
搜索结果使用列表展示:
body: _controller.text.isEmpty
? _buildHotSearches()
: _buildSearchResults(),
根据搜索框是否为空,显示不同的内容。为空时显示热门搜索,不为空时显示搜索结果。
Widget _buildSearchResults() {
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: searchResults.length,
itemBuilder: (context, index) {
return _buildResultItem(searchResults[index]);
},
);
}
搜索结果使用 ListView.builder 展示。实际开发中,searchResults 应该根据搜索关键词从数据库或网络获取。
Widget _buildResultItem(Recipe recipe) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Container(
width: 60.w,
height: 60.h,
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(Icons.restaurant, size: 30.sp, color: Colors.orange),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
recipe.name,
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 4.h),
Text(
recipe.description,
style: TextStyle(fontSize: 11.sp, color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
搜索结果项和其他列表项类似,包含图片、名称和描述。描述使用单行显示,超出部分用省略号表示。
高亮搜索关键词
为了让用户更容易找到匹配的部分,可以高亮显示搜索关键词:
Text.rich(
TextSpan(
children: _highlightKeyword(recipe.name, _controller.text),
),
)
_highlightKeyword 方法将文本分割成多个部分,匹配的部分使用不同的样式:
List<TextSpan> _highlightKeyword(String text, String keyword) {
if (keyword.isEmpty) {
return [TextSpan(text: text)];
}
final List<TextSpan> spans = [];
final lowerText = text.toLowerCase();
final lowerKeyword = keyword.toLowerCase();
int start = 0;
int index = lowerText.indexOf(lowerKeyword);
while (index != -1) {
if (index > start) {
spans.add(TextSpan(text: text.substring(start, index)));
}
spans.add(TextSpan(
text: text.substring(index, index + keyword.length),
style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
));
start = index + keyword.length;
index = lowerText.indexOf(lowerKeyword, start);
}
if (start < text.length) {
spans.add(TextSpan(text: text.substring(start)));
}
return spans;
}
这个方法找到所有匹配的位置,将匹配的部分用橙色粗体显示,其他部分用默认样式。
添加搜索历史
用户可能会重复搜索相同的关键词,可以保存搜索历史:
List<String> searchHistory = [];
void _performSearch(String keyword) {
if (keyword.isNotEmpty && !searchHistory.contains(keyword)) {
setState(() {
searchHistory.insert(0, keyword);
if (searchHistory.length > 10) {
searchHistory.removeLast();
}
});
}
// 执行搜索
}
每次搜索时,将关键词添加到历史记录。使用 insert(0, keyword) 将新记录添加到开头,保持最近的记录在前面。
限制历史记录数量为 10,超过时删除最旧的记录。这样可以避免历史记录无限增长。
搜索历史可以显示在热门搜索下方:
if (searchHistory.isNotEmpty) {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('搜索历史', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
TextButton(
onPressed: () {
setState(() {
searchHistory.clear();
});
},
child: Text('清空'),
),
],
),
),
...searchHistory.map((keyword) => ListTile(
leading: Icon(Icons.history),
title: Text(keyword),
onTap: () {
_controller.text = keyword;
_performSearch(keyword);
},
)),
],
),
}
搜索历史使用 ListTile 展示,每个历史记录可以点击重新搜索。右上角有一个"清空"按钮,可以清空所有历史记录。
添加空状态
如果搜索没有结果,需要显示空状态:
if (searchResults.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('没有找到相关菜谱', style: TextStyle(color: Colors.grey)),
],
),
);
}
空状态使用搜索图标和文字说明,让用户知道这是正常的情况,不是出错了。
优化搜索性能
如果菜谱数量很多,搜索可能会比较慢。可以使用全文搜索引擎,或者在数据库中建立索引:
Future<List<Recipe>> searchRecipes(String keyword) async {
final db = await database;
return await db.query(
'recipes',
where: 'name LIKE ? OR description LIKE ?',
whereArgs: ['%$keyword%', '%$keyword%'],
);
}
使用 LIKE 查询可以匹配包含关键词的菜谱。但 LIKE 查询性能不好,如果数据量大,应该使用全文搜索。
总结
菜谱搜索功能使用实时搜索的方式,用户输入时立即显示结果。热门搜索标签和搜索历史能帮助用户快速开始搜索。
通过合理的交互设计和性能优化,我们让搜索功能既快速又准确。用户可以轻松找到想要的菜谱,不用在海量数据中慢慢翻找。
下一篇文章我们将实现菜谱详情展示功能,让用户能够查看完整的菜谱信息。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)