Flutter for OpenHarmony 教育百科实战:大学搜索
本文介绍了大学搜索功能的实现方案,重点优化了用户体验。该功能基于Hipolabs开放API,支持全球大学信息检索。实现过程中采用多种状态管理:未搜索时显示引导提示、搜索中展示骨架屏、无结果时提供友好提示。页面布局包含搜索框和结果列表,搜索结果以卡片形式展示大学名称、国家和官网链接。通过_hasSearched状态变量区分不同场景,确保搜索流程顺畅。技术实现上使用Flutter框架,包含状态管理、A
大学搜索功能让用户可以查找全球各地的大学信息。这个页面和图书搜索类似,但数据结构不太一样,而且大学信息里有官网链接,可以直接跳转到大学官网。
做这个页面的时候,我特别注意了搜索体验:未搜索时显示引导提示,搜索中显示骨架屏,无结果时显示友好提示。这些细节加起来,让整个搜索流程变得顺畅。

状态变量设计
大学搜索页面需要管理搜索状态和结果:
class UniversityListScreen extends StatefulWidget {
const UniversityListScreen({super.key});
State<UniversityListScreen> createState() => _UniversityListScreenState();
}
class _UniversityListScreenState extends State<UniversityListScreen> {
final _searchController = TextEditingController();
List<dynamic> _universities = [];
bool _isLoading = false;
bool _hasSearched = false;
}
_hasSearched区分"还没搜索"和"搜索结果为空"两种状态。这个变量在图书搜索里也用到了,是搜索页面的标配。
搜索方法
调用API搜索大学:
Future<void> _search() async {
if (_searchController.text.isEmpty) return;
setState(() {
_isLoading = true;
_hasSearched = true;
});
try {
final universities = await ApiService.searchUniversities(_searchController.text);
if (mounted) {
setState(() {
_universities = universities;
_isLoading = false;
});
}
} catch (e) {
print('search universities error: $e');
if (mounted) {
setState(() {
_universities = [];
_isLoading = false;
});
}
}
}
搜索前检查输入是否为空,避免无效请求。即使请求失败也要更新状态,让页面显示正确的内容(空结果提示)。
关于API
大学搜索用的是Hipolabs的Universities API,这是一个免费的开放API,包含全球几千所大学的信息。搜索时可以按名称或国家搜索。
页面布局
搜索框和结果列表的组合:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('搜索大学'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '输入大学名称...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_universities = [];
_hasSearched = false;
});
},
)
: null,
),
onSubmitted: (_) => _search(),
onChanged: (_) => setState(() {}),
),
),
Expanded(child: _buildResults()),
],
),
);
}
清除按钮点击后重置所有状态,回到初始的引导界面。
onSubmitted在用户按回车时触发搜索。
结果区域构建
根据不同状态显示不同内容:
Widget _buildResults() {
if (!_hasSearched) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.school, size: 80, color: Colors.grey[300]),
const SizedBox(height: 16),
Text('搜索全球大学', style: TextStyle(color: Colors.grey[500])),
const SizedBox(height: 8),
Text('输入大学名称开始搜索', style: TextStyle(color: Colors.grey[400], fontSize: 12)),
],
),
);
}
未搜索时显示引导提示,一个大的学校图标加两行引导语。这比显示空白页面友好多了。
if (_isLoading) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) => const Padding(
padding: EdgeInsets.only(bottom: 12),
child: LoadingShimmer(height: 80),
),
);
}
if (_universities.isEmpty) {
return const EmptyWidget(message: '没有找到相关大学', icon: Icons.school);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _universities.length,
itemBuilder: (context, index) => _buildUniversityItem(_universities[index]),
);
}
加载中显示骨架屏,无结果显示空状态,有结果显示列表。骨架屏的高度设为80,和实际的大学卡片高度接近。
大学项展示
每所大学用卡片展示:
Widget _buildUniversityItem(Map<String, dynamic> university) {
final name = university['name'] ?? '未知';
final country = university['country'] ?? '未知';
final webPages = university['web_pages'] as List?;
final domains = university['domains'] as List?;
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showUniversityDetail(context, university),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.school, color: Theme.of(context).colorScheme.primary),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.location_on, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text(country, style: TextStyle(color: Colors.grey[600], fontSize: 13)),
],
),
],
),
),
],
),
左侧是一个带背景的学校图标,右侧显示大学名称和所在国家。位置图标让国家信息更直观。
if (domains != null && domains.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
domains.first,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
],
if (webPages != null && webPages.isNotEmpty) ...[
const SizedBox(height: 8),
TextButton.icon(
onPressed: () => _launchUrl(webPages.first),
icon: const Icon(Icons.open_in_new, size: 16),
label: const Text('访问官网'),
style: TextButton.styleFrom(padding: EdgeInsets.zero),
),
],
],
),
),
),
);
}
域名用小号灰色字显示,官网链接用TextButton,点击后在浏览器中打开。
padding: EdgeInsets.zero去掉按钮的默认内边距,让它和其他内容对齐。
打开网页
使用url_launcher打开大学官网:
Future<void> _launchUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
canLaunchUrl检查是否可以打开URL,externalApplication模式在外部浏览器中打开。如果用inAppWebView模式,会在App内部打开,但需要额外配置。
关于url_launcher
这是Flutter官方的插件,用于打开URL、发送邮件、拨打电话等。使用前需要在pubspec.yaml里添加依赖:
dependencies:
url_launcher: ^6.1.0
大学详情弹窗
点击卡片显示详情弹窗:
void _showUniversityDetail(BuildContext context, Map<String, dynamic> university) {
final name = university['name'] ?? '未知';
final country = university['country'] ?? '未知';
final stateProvince = university['state-province'];
final webPages = university['web_pages'] as List?;
final domains = university['domains'] as List?;
final alphaTwoCode = university['alpha_two_code'] ?? '';
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.9,
expand: false,
builder: (context, scrollController) => Container(
padding: const EdgeInsets.all(20),
child: ListView(
controller: scrollController,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 20),
// 详情内容...
],
),
),
),
);
}
用
DraggableScrollableSheet创建可拖动的底部弹窗,用户可以上下拖动调整高度。顶部的小横条是拖动指示器,告诉用户这个弹窗可以拖动。
为什么用底部弹窗而不是新页面?
因为大学的信息不多,用弹窗展示就够了,不需要跳转到新页面。而且弹窗可以快速关闭,用户想看下一所大学时更方便。
资源释放
页面销毁时释放控制器:
void dispose() {
_searchController.dispose();
super.dispose();
}
小结
大学搜索页面展示了如何实现一个完整的搜索功能。通过_hasSearched状态区分不同场景,让用户在各种情况下都能得到合适的反馈。url_launcher的使用让用户可以直接访问大学官网,获取更多信息。底部弹窗的设计让详情展示更轻量,不需要跳转页面。
下一篇我们来看百科搜索功能的实现,了解如何接入维基百科API。
本文是Flutter for OpenHarmony教育百科实战系列的第十一篇。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)