【Flutter for OpenHarmony第三方库】Flutter for OpenHarmony搜索功能实战指南:从0到1打造高性能搜索体验
本文详细介绍了如何在Flutter for OpenHarmony应用中实现一个完整的搜索功能,包括UI设计、搜索逻辑、空状态提示和性能优化。通过实战案例,我们展示了如何构建一个高性能、用户友好的搜索系统。未来,我们可以进一步扩展搜索功能,例如添加历史记录、热门搜索、语音搜索等。同时,我们可以结合鸿蒙系统的特性,实现更多个性化的搜索体验。
Flutter for OpenHarmony搜索功能实战指南:从0到1打造高性能搜索体验
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
一、引言:搜索功能的重要性
在移动应用开发中,搜索功能是提升用户体验的关键组件。随着数据量的增长,用户需要更高效的方式找到所需信息。对于Flutter for OpenHarmony跨平台应用而言,一个优秀的搜索功能不仅要满足基本的查找需求,还要兼顾性能、美观和用户体验。
本文将详细介绍如何在Flutter for OpenHarmony应用中实现一个完整的搜索功能,包括UI设计、搜索逻辑、空状态提示和性能优化。我们将使用真实的网络数据,通过实战案例展示如何构建一个高性能、用户友好的搜索系统。
二、技术选型与架构设计
2.1 技术栈选择
- Flutter for OpenHarmony:跨平台UI框架,支持一次开发多端部署
- Dio:HTTP客户端库,用于网络请求
- StatefulWidget:状态管理,实现搜索状态的实时更新
- Timer:防抖处理,优化搜索性能
2.2 架构设计
我们将采用分层架构设计,将搜索功能分为以下几个模块:
- UI层:搜索框、列表展示、空状态提示
- 业务逻辑层:搜索逻辑、防抖处理、数据筛选
- 数据层:网络请求、数据缓存
三、实战开发:实现搜索功能
3.1 项目初始化
首先,我们需要创建一个Flutter for OpenHarmony项目,并添加必要的依赖。在pubspec.yaml文件中添加:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0
3.2 数据模型与网络请求
我们将使用JSONPlaceholder提供的Todo API作为数据源。首先定义数据模型:
class TodoItem {
final int userId;
final int id;
final String title;
final bool completed;
TodoItem({
required this.userId,
required this.id,
required this.title,
required this.completed,
});
factory TodoItem.fromJson(Map<String, dynamic> json) {
return TodoItem(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
completed: json['completed'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'userId': userId,
'id': id,
'title': title,
'completed': completed,
};
}
}
接下来实现网络请求服务:
class TodoService {
static const String _baseUrl = 'https://jsonplaceholder.typicode.com';
final Dio _dio = Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
Future<List<TodoItem>> getTodos() async {
try {
final response = await _dio.get('$_baseUrl/todos');
final List<dynamic> data = response.data;
return data.map((json) => TodoItem.fromJson(json)).toList();
} on DioException catch (e) {
throw Exception(_handleError(e));
}
}
Future<TodoItem> getTodoById(int id) async {
try {
final response = await _dio.get('$_baseUrl/todos/$id');
return TodoItem.fromJson(response.data);
} on DioException catch (e) {
throw Exception(_handleError(e));
}
}
String _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return 'Connection timeout';
case DioExceptionType.sendTimeout:
return 'Send timeout';
case DioExceptionType.receiveTimeout:
return 'Receive timeout';
case DioExceptionType.badResponse:
return 'Server error: ${e.response?.statusCode}';
case DioExceptionType.cancel:
return 'Request cancelled';
case DioExceptionType.connectionError:
return 'Connection error';
default:
return 'Unknown error: ${e.message}';
}
}
}
3.3 搜索框UI设计
搜索框是搜索功能的入口,需要设计得美观且易用。我们将实现一个带有圆角、边框高亮和清除按钮的搜索框:
Widget _buildSearchBar() {
return Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _isSearching ? Colors.blue.shade300 : Colors.transparent,
width: 1.5,
),
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search todos...',
hintStyle: TextStyle(color: Colors.grey.shade500),
prefixIcon: Icon(
Icons.search,
color: _isSearching ? Colors.blue : Colors.grey,
),
suffixIcon: _isSearching
? IconButton(
icon: const Icon(Icons.clear, color: Colors.grey),
onPressed: _clearSearch,
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
style: const TextStyle(fontSize: 15),
),
);
}
3.4 搜索逻辑实现
搜索逻辑是搜索功能的核心,需要实现本地筛选和多条件搜索:
List<TodoItem> get _filteredTodos {
if (_searchQuery.isEmpty) {
return _todos;
}
return _todos.where((todo) {
final titleMatch = todo.title.toLowerCase().contains(_searchQuery);
final idMatch = todo.id.toString().contains(_searchQuery);
final userIdMatch = todo.userId.toString().contains(_searchQuery);
return titleMatch || idMatch || userIdMatch;
}).toList();
}
3.5 防抖处理
为了优化搜索性能,我们需要实现防抖处理,避免频繁触发搜索:
void _onSearchChanged() {
if (_debounceTimer?.isActive ?? false) {
_debounceTimer!.cancel();
}
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
setState(() {
_searchQuery = _searchController.text.toLowerCase().trim();
_isSearching = _searchQuery.isNotEmpty;
});
});
}
3.6 空状态提示
当搜索结果为空时,需要提供友好的提示信息:
if (_filteredTodos.isEmpty) {
if (_isSearching) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'No results found',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Try different keywords: "$_searchQuery"',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: _clearSearch,
icon: const Icon(Icons.clear),
label: const Text('Clear Search'),
),
],
),
);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'No data available',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
],
),
);
}
四、完整代码实现
以下是完整的搜索功能实现代码:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
void main() {
runApp(const MyApp());
}
class TodoItem {
final int userId;
final int id;
final String title;
final bool completed;
TodoItem({
required this.userId,
required this.id,
required this.title,
required this.completed,
});
factory TodoItem.fromJson(Map<String, dynamic> json) {
return TodoItem(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
completed: json['completed'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'userId': userId,
'id': id,
'title': title,
'completed': completed,
};
}
}
class TodoService {
static const String _baseUrl = 'https://jsonplaceholder.typicode.com';
final Dio _dio = Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
Future<List<TodoItem>> getTodos() async {
try {
final response = await _dio.get('$_baseUrl/todos');
final List<dynamic> data = response.data;
return data.map((json) => TodoItem.fromJson(json)).toList();
} on DioException catch (e) {
throw Exception(_handleError(e));
}
}
Future<TodoItem> getTodoById(int id) async {
try {
final response = await _dio.get('$_baseUrl/todos/$id');
return TodoItem.fromJson(response.data);
} on DioException catch (e) {
throw Exception(_handleError(e));
}
}
String _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return 'Connection timeout';
case DioExceptionType.sendTimeout:
return 'Send timeout';
case DioExceptionType.receiveTimeout:
return 'Receive timeout';
case DioExceptionType.badResponse:
return 'Server error: ${e.response?.statusCode}';
case DioExceptionType.cancel:
return 'Request cancelled';
case DioExceptionType.connectionError:
return 'Connection error';
default:
return 'Unknown error: ${e.message}';
}
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'OpenHarmony Todo List',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const TodoListPage(),
);
}
}
class TodoListPage extends StatefulWidget {
const TodoListPage({super.key});
State<TodoListPage> createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
final TodoService _todoService = TodoService();
List<TodoItem> _todos = [];
bool _isLoading = true;
String? _errorMessage;
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
Timer? _debounceTimer;
bool _isSearching = false;
void initState() {
super.initState();
_loadTodos();
_searchController.addListener(_onSearchChanged);
}
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
void _onSearchChanged() {
if (_debounceTimer?.isActive ?? false) {
_debounceTimer!.cancel();
}
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
setState(() {
_searchQuery = _searchController.text.toLowerCase().trim();
_isSearching = _searchQuery.isNotEmpty;
});
});
}
void _clearSearch() {
_searchController.clear();
setState(() {
_searchQuery = '';
_isSearching = false;
});
}
List<TodoItem> get _filteredTodos {
if (_searchQuery.isEmpty) {
return _todos;
}
return _todos.where((todo) {
final titleMatch = todo.title.toLowerCase().contains(_searchQuery);
final idMatch = todo.id.toString().contains(_searchQuery);
final userIdMatch = todo.userId.toString().contains(_searchQuery);
return titleMatch || idMatch || userIdMatch;
}).toList();
}
Future<void> _loadTodos() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final todos = await _todoService.getTodos();
setState(() {
_todos = todos;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Todo List'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadTodos,
),
],
),
body: Column(
children: [
_buildSearchBar(),
Expanded(child: _buildBody()),
],
),
);
}
Widget _buildSearchBar() {
return Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _isSearching ? Colors.blue.shade300 : Colors.transparent,
width: 1.5,
),
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search todos...',
hintStyle: TextStyle(color: Colors.grey.shade500),
prefixIcon: Icon(
Icons.search,
color: _isSearching ? Colors.blue : Colors.grey,
),
suffixIcon: _isSearching
? IconButton(
icon: const Icon(Icons.clear, color: Colors.grey),
onPressed: _clearSearch,
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
style: const TextStyle(fontSize: 15),
),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading data from network...'),
],
),
);
}
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Error: $_errorMessage',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadTodos,
child: const Text('Retry'),
),
],
),
);
}
if (_filteredTodos.isEmpty) {
if (_isSearching) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'No results found',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Try different keywords: "$_searchQuery"',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: _clearSearch,
icon: const Icon(Icons.clear),
label: const Text('Clear Search'),
),
],
),
);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'No data available',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
],
),
);
}
return ListView.builder(
itemCount: _filteredTodos.length,
itemBuilder: (context, index) {
final todo = _filteredTodos[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: todo.completed ? Colors.green : Colors.orange,
child: Icon(
todo.completed ? Icons.check : Icons.pending,
color: Colors.white,
),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: Text('User ID: ${todo.userId} | ID: ${todo.id}'),
trailing: Checkbox(value: todo.completed, onChanged: null),
),
);
},
);
}
}
五、鸿蒙设备验证
5.1 编译运行
将代码部署到鸿蒙设备上,验证搜索功能是否正常工作。在DevEco Studio中,选择鸿蒙设备作为目标设备,点击运行按钮。
5.2 测试场景
- 正常搜索:输入关键词,验证搜索结果是否正确
- 空搜索:不输入关键词,验证是否显示全部数据
- 无结果搜索:输入不存在的关键词,验证空状态提示
- 性能测试:快速输入多个关键词,验证防抖效果
5.3 运行截图
以下是搜索功能在鸿蒙设备上的运行截图:

六、性能优化与最佳实践
6.1 性能优化
- 防抖处理:减少不必要的搜索请求
- 本地筛选:避免频繁网络请求
- 状态管理:优化UI更新时机
6.2 最佳实践
- 用户体验:提供清晰的搜索反馈
- 错误处理:完善的异常处理机制
- 可维护性:模块化设计,代码复用
七、总结与展望
本文详细介绍了如何在Flutter for OpenHarmony应用中实现一个完整的搜索功能,包括UI设计、搜索逻辑、空状态提示和性能优化。通过实战案例,我们展示了如何构建一个高性能、用户友好的搜索系统。
未来,我们可以进一步扩展搜索功能,例如添加历史记录、热门搜索、语音搜索等。同时,我们可以结合鸿蒙系统的特性,实现更多个性化的搜索体验。
八、社区资源
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Flutter for OpenHarmony官方文档:https://developer.harmonyos.com/cn/docs/documentation/doc-guides/flutter-overview-0000001108767065
- Dio官方文档:https://pub.dev/packages/dio
通过本文的学习,相信你已经掌握了在Flutter for OpenHarmony应用中实现搜索功能的方法。希望本文能对你的开发工作有所帮助,祝你在鸿蒙跨平台开发的道路上取得更大的成功!
更多推荐




所有评论(0)