Flutter for OpenHarmony垃圾分类指南App实战:搜索结果实现
本文介绍了Flutter for OpenHarmony环境下搜索结果页面的实现方案。通过GetX路由传参接收搜索关键词,使用ListView.builder高效渲染结果列表。页面包含AppBar显示搜索词、空结果友好提示(含图标和操作建议)、分类标签颜色映射等核心功能。重点技术包括:Material Design组件组合、搜索结果匹配算法实现、空状态UI设计原则(说明情况+提供建议+操作引导)。

前言
搜索结果页面是一个独立的结果展示页,跟搜索页面里的实时搜索结果不太一样。这个页面主要用在从其他入口(比如热门搜索标签)跳转过来的场景,进来就直接显示搜索结果,不用再输入。
本文将详细介绍如何在Flutter for OpenHarmony环境下实现一个完整的搜索结果页面,包括路由参数传递、搜索逻辑实现、空结果处理、结果列表展示以及分类标签的颜色设计等核心技术点。
技术要点概览
在开始实现之前,让我们先了解本页面涉及的核心技术点:
- GetX路由传参:通过Get.arguments接收搜索关键词
- ListView.builder:高效的列表渲染
- 空结果设计:友好的无结果提示
- 颜色映射:根据垃圾类型显示对应颜色标签
- 搜索算法:关键词匹配的实现策略
- Material Design:Card和ListTile的组合使用
接收搜索关键词
页面通过路由参数接收要搜索的关键词,这是GetX路由传参的标准方式:
class SearchResultPage extends StatelessWidget {
const SearchResultPage({super.key});
Widget build(BuildContext context) {
// 从路由参数获取搜索关键词
// 如果没有传参,使用空字符串作为默认值
final String keyword = Get.arguments ?? '';
// 执行搜索,获取匹配的结果列表
final results = GarbageData.searchItems(keyword);
路由跳转方式
Get.arguments是GetX路由传参的方式,跳转的时候这样写:
// 方式1:使用命名路由
Get.toNamed(Routes.searchResult, arguments: '塑料瓶');
// 方式2:使用Get.to
Get.to(() => const SearchResultPage(), arguments: '塑料瓶');
// 方式3:传递复杂参数
Get.toNamed(Routes.searchResult, arguments: {
'keyword': '塑料瓶',
'category': 'recyclable',
});
拿到关键词后直接调用GarbageData.searchItems进行搜索。这个方法是我们封装好的,传入关键词返回匹配的垃圾列表。
为啥不用Controller:这个页面比较简单,就是展示一下搜索结果,没有复杂的状态管理需求。直接在build方法里搜索就行,不用专门搞个Controller。这种设计遵循了"简单问题简单解决"的原则。
何时需要Controller
虽然这个页面不需要Controller,但以下情况建议使用:
- 需要分页加载:结果数量很大,需要分页
- 需要筛选排序:用户可以对结果进行筛选或排序
- 需要缓存结果:避免重复搜索
- 需要状态管理:如加载状态、错误状态等
// 如果需要更复杂的功能,可以使用Controller
class SearchResultController extends GetxController {
final keyword = ''.obs;
final results = <GarbageItem>[].obs;
final isLoading = false.obs;
final errorMessage = ''.obs;
void onInit() {
super.onInit();
keyword.value = Get.arguments ?? '';
search();
}
Future<void> search() async {
isLoading.value = true;
errorMessage.value = '';
try {
results.value = await GarbageData.searchItems(keyword.value);
} catch (e) {
errorMessage.value = '搜索失败,请重试';
} finally {
isLoading.value = false;
}
}
}
AppBar显示搜索词
让用户知道当前搜的是什么,这是良好用户体验的基本要求:
return Scaffold(
appBar: AppBar(
title: Text('搜索: $keyword'),
// 可以添加更多操作按钮
actions: [
// 重新搜索按钮
IconButton(
icon: const Icon(Icons.search),
onPressed: () => Get.toNamed(Routes.search),
),
],
),
标题直接显示"搜索: xxx",简单明了。用户一眼就能知道当前页面展示的是什么关键词的搜索结果。
AppBar增强设计
可以在AppBar中添加更多功能:
AppBar(
title: Text('搜索: $keyword'),
actions: [
// 结果数量提示
Center(
child: Padding(
padding: EdgeInsets.only(right: 8.w),
child: Text(
'${results.length}个结果',
style: TextStyle(fontSize: 14.sp),
),
),
),
// 筛选按钮
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () => _showFilterDialog(context),
),
],
)
空结果的处理
搜不到东西的时候,得给用户一个友好的提示:
body: results.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('未找到相关结果', style: TextStyle(fontSize: 16.sp, color: Colors.grey)),
SizedBox(height: 8.h),
Text('试试其他关键词', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
],
),
)
空结果设计原则
用了Icons.search_off这个图标,一个带斜杠的放大镜,表示"搜不到"。下面两行文字,一行说明情况,一行给建议,比光秃秃一个空白页面好多了。
设计原则:空结果页面应该做到三点:
- 说明情况:告诉用户没有找到结果
- 给出建议:提示用户可以尝试其他关键词
- 提供操作:可以添加按钮让用户快速返回搜索
增强版空结果设计
Widget _buildEmptyResult(String keyword) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 使用更大的图标
Icon(Icons.search_off, size: 80.sp, color: Colors.grey.shade300),
SizedBox(height: 24.h),
Text(
'未找到"$keyword"相关结果',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
SizedBox(height: 8.h),
Text(
'请尝试以下操作:',
style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade400),
),
SizedBox(height: 16.h),
// 建议列表
_buildSuggestionItem('检查关键词是否有错别字'),
_buildSuggestionItem('尝试使用更简短的关键词'),
_buildSuggestionItem('尝试使用物品的别名'),
SizedBox(height: 24.h),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton.icon(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back),
label: const Text('返回'),
),
SizedBox(width: 16.w),
ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.search),
icon: const Icon(Icons.search),
label: const Text('重新搜索'),
),
],
),
],
),
);
}
Widget _buildSuggestionItem(String text) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.lightbulb_outline, size: 16.sp, color: Colors.amber),
SizedBox(width: 8.w),
Text(text, style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
],
),
);
}
结果列表的展示
有结果时用ListView展示:
: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: results.length,
itemBuilder: (context, index) {
final item = results[index];
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: Text(item.icon, style: TextStyle(fontSize: 28.sp)),
title: Text(item.name),
subtitle: Text(item.description),
列表项内容说明
每个结果项显示:
- 图标:emoji表情,直观易识别
- 名称:垃圾的名字,主要信息
- 描述:简短的说明,辅助信息
关键词高亮显示
为了让用户更容易找到匹配的内容,可以对关键词进行高亮显示:
Widget _buildHighlightedText(String text, String keyword) {
if (keyword.isEmpty) {
return Text(text);
}
final spans = <TextSpan>[];
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: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
),
));
start = index + keyword.length;
index = lowerText.indexOf(lowerKeyword, start);
}
// 添加剩余文本
if (start < text.length) {
spans.add(TextSpan(text: text.substring(start)));
}
return RichText(
text: TextSpan(
style: TextStyle(fontSize: 14.sp, color: Colors.black),
children: spans,
),
);
}
分类标签的设计
列表项右边显示分类标签,用对应的颜色:
trailing: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: _getTypeColor(item.type).withOpacity(0.2),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
item.typeName,
style: TextStyle(fontSize: 12.sp, color: _getTypeColor(item.type)),
),
),
onTap: () => Get.toNamed(Routes.itemDetail, arguments: item),
),
);
},
),
);
}
颜色设计原则
颜色设计:标签背景用分类颜色的20%透明度版本,文字用分类颜色。这样既能区分不同分类,又不会太刺眼。可回收物是蓝色标签,有害垃圾是红色标签,一眼就能看出来。
点击结果跳转到物品详情页,把完整的物品对象传过去。
标签组件封装
可以将标签封装为独立组件,方便复用:
class GarbageTypeTag extends StatelessWidget {
final GarbageType type;
final String typeName;
const GarbageTypeTag({
super.key,
required this.type,
required this.typeName,
});
Widget build(BuildContext context) {
final color = _getTypeColor(type);
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(4.r),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 可以添加小图标
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
SizedBox(width: 4.w),
Text(
typeName,
style: TextStyle(
fontSize: 12.sp,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Color _getTypeColor(GarbageType type) {
switch (type) {
case GarbageType.recyclable:
return AppTheme.recyclableColor;
case GarbageType.hazardous:
return AppTheme.hazardousColor;
case GarbageType.kitchen:
return AppTheme.kitchenColor;
default:
return AppTheme.otherColor;
}
}
}
颜色映射方法
根据垃圾类型返回对应颜色:
Color _getTypeColor(type) {
switch (type.toString()) {
case 'GarbageType.recyclable':
return AppTheme.recyclableColor;
case 'GarbageType.hazardous':
return AppTheme.hazardousColor;
case 'GarbageType.kitchen':
return AppTheme.kitchenColor;
default:
return AppTheme.otherColor;
}
}
}
四种垃圾分类颜色
四种类型四种颜色:
- 可回收物 → 蓝色(#1E88E5)
- 有害垃圾 → 红色(#E53935)
- 厨余垃圾 → 绿色(#43A047)
- 其他垃圾 → 灰色(#757575)
这个配色跟现实中垃圾桶的颜色一致,用户不用学就能记住。这种设计遵循了"符合用户心智模型"的原则。
颜色常量定义
建议在主题文件中统一定义颜色常量:
class AppTheme {
// 垃圾分类颜色
static const Color recyclableColor = Color(0xFF1E88E5); // 蓝色 - 可回收物
static const Color hazardousColor = Color(0xFFE53935); // 红色 - 有害垃圾
static const Color kitchenColor = Color(0xFF43A047); // 绿色 - 厨余垃圾
static const Color otherColor = Color(0xFF757575); // 灰色 - 其他垃圾
// 获取颜色的便捷方法
static Color getGarbageTypeColor(GarbageType type) {
switch (type) {
case GarbageType.recyclable:
return recyclableColor;
case GarbageType.hazardous:
return hazardousColor;
case GarbageType.kitchen:
return kitchenColor;
default:
return otherColor;
}
}
}
搜索逻辑的实现
GarbageData.searchItems方法是这样实现的:
class GarbageData {
static List<GarbageItem> searchItems(String keyword) {
// 空关键词返回空列表
if (keyword.isEmpty) return [];
// 转换为小写进行不区分大小写的搜索
final lowerKeyword = keyword.toLowerCase();
return allItems.where((item) =>
item.name.toLowerCase().contains(lowerKeyword) ||
item.description.toLowerCase().contains(lowerKeyword)
).toList();
}
}
搜索会同时匹配名称和描述。比如用户搜"瓶",能找到"塑料瓶"、“玻璃瓶”;搜"饮料",也能找到描述里包含"饮料"的物品。
搜索策略:这里用的是简单的包含匹配。如果数据量大或者需要更智能的搜索(比如拼音搜索、模糊匹配),可以考虑用专门的搜索库或者后端搜索服务。
增强版搜索实现
class GarbageData {
/// 增强版搜索方法
/// 支持多关键词、别名匹配、相关度排序
static List<GarbageItem> searchItemsAdvanced(String keyword) {
if (keyword.isEmpty) return [];
final lowerKeyword = keyword.toLowerCase().trim();
final keywords = lowerKeyword.split(' ').where((k) => k.isNotEmpty).toList();
// 计算每个物品的匹配分数
final scoredItems = <MapEntry<GarbageItem, int>>[];
for (var item in allItems) {
int score = 0;
for (var kw in keywords) {
// 名称完全匹配,高分
if (item.name.toLowerCase() == kw) {
score += 100;
}
// 名称包含关键词
else if (item.name.toLowerCase().contains(kw)) {
score += 50;
}
// 别名匹配
if (item.aliases.any((alias) => alias.toLowerCase().contains(kw))) {
score += 30;
}
// 描述包含关键词
if (item.description.toLowerCase().contains(kw)) {
score += 10;
}
}
if (score > 0) {
scoredItems.add(MapEntry(item, score));
}
}
// 按分数降序排序
scoredItems.sort((a, b) => b.value.compareTo(a.value));
return scoredItems.map((e) => e.key).toList();
}
/// 拼音搜索支持
static List<GarbageItem> searchByPinyin(String keyword) {
if (keyword.isEmpty) return [];
final lowerKeyword = keyword.toLowerCase();
return allItems.where((item) {
// 匹配名称
if (item.name.toLowerCase().contains(lowerKeyword)) return true;
// 匹配拼音全拼
if (item.pinyinFull.contains(lowerKeyword)) return true;
// 匹配拼音首字母
if (item.pinyinInitials.contains(lowerKeyword)) return true;
return false;
}).toList();
}
}
这个页面的使用场景
搜索结果页面主要用在这些场景:
1. 热门搜索
用户点击热门标签,直接跳到这个页面显示结果:
// 热门搜索标签点击
GestureDetector(
onTap: () => Get.toNamed(Routes.searchResult, arguments: '塑料瓶'),
child: Chip(label: Text('塑料瓶')),
)
2. 推荐搜索
App推荐某个关键词,用户点击后进入:
// 推荐搜索卡片点击
Card(
child: ListTile(
title: Text('今日推荐:电池'),
onTap: () => Get.toNamed(Routes.searchResult, arguments: '电池'),
),
)
3. 外部跳转
从其他App或者网页跳转过来搜索:
// 处理深度链接
void handleDeepLink(Uri uri) {
if (uri.path == '/search') {
final keyword = uri.queryParameters['keyword'];
if (keyword != null) {
Get.toNamed(Routes.searchResult, arguments: keyword);
}
}
}
与搜索页面的区别
跟搜索页面的实时搜索相比,这个页面更适合"一次性"的搜索场景,进来就看结果,不需要再输入或修改关键词。
| 特性 | 搜索页面 | 搜索结果页面 |
|---|---|---|
| 输入框 | 有,可编辑 | 无 |
| 实时搜索 | 支持 | 不支持 |
| 搜索历史 | 显示 | 不显示 |
| 使用场景 | 主动搜索 | 跳转搜索 |
性能优化建议
1. 使用const构造函数
const Icon(Icons.search_off, size: 64, color: Colors.grey)
const Icon(Icons.arrow_forward_ios, size: 16)
2. 列表项使用Key
return Card(
key: ValueKey(item.id),
// ...
);
3. 搜索结果缓存
class SearchCache {
static final Map<String, List<GarbageItem>> _cache = {};
static List<GarbageItem> search(String keyword) {
if (_cache.containsKey(keyword)) {
return _cache[keyword]!;
}
final results = GarbageData.searchItems(keyword);
_cache[keyword] = results;
return results;
}
static void clear() {
_cache.clear();
}
}
4. 使用RepaintBoundary
RepaintBoundary(
child: Card(
// 复杂的卡片内容
),
)
可扩展方向
1. 筛选功能
按垃圾类型筛选结果:
final selectedTypes = <GarbageType>{}.obs;
List<GarbageItem> get filteredResults {
if (selectedTypes.isEmpty) return results;
return results.where((item) => selectedTypes.contains(item.type)).toList();
}
2. 排序功能
按名称、相关度排序:
enum SortType { relevance, name, type }
final sortType = SortType.relevance.obs;
List<GarbageItem> get sortedResults {
final list = List<GarbageItem>.from(filteredResults);
switch (sortType.value) {
case SortType.name:
list.sort((a, b) => a.name.compareTo(b.name));
break;
case SortType.type:
list.sort((a, b) => a.typeName.compareTo(b.typeName));
break;
default:
break;
}
return list;
}
3. 分页加载
结果数量很大时使用分页:
class SearchResultController extends GetxController {
final results = <GarbageItem>[].obs;
final isLoading = false.obs;
final hasMore = true.obs;
int _page = 1;
final int _pageSize = 20;
Future<void> loadMore() async {
if (isLoading.value || !hasMore.value) return;
isLoading.value = true;
final newItems = await _searchPage(_page, _pageSize);
if (newItems.length < _pageSize) {
hasMore.value = false;
}
results.addAll(newItems);
_page++;
isLoading.value = false;
}
}
总结
搜索结果页面是一个简单但重要的页面,它为用户提供了快速查看搜索结果的入口。本文介绍的实现方案包括:
- 路由参数传递:使用GetX的Get.arguments接收搜索关键词
- 空结果处理:友好的无结果提示和建议
- 结果列表展示:使用ListView.builder高效渲染
- 分类标签设计:颜色与垃圾分类标准一致
- 搜索逻辑实现:支持名称和描述的匹配
通过合理的页面设计和良好的用户体验,搜索结果页面可以帮助用户快速找到所需的垃圾分类信息。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)