Flutter for OpenHarmony 教育百科实战:探索
本文介绍了教育百科App探索页的设计与实现。该页面采用分类卡片布局,将功能入口清晰组织为搜索入口和各分类模块。技术实现上使用StatelessWidget构建静态页面,通过封装_buildCategorySection方法复用分类模块样式,并定义_CategoryItem数据结构管理功能项。页面设计注重用户体验,包括合理的间距设置、颜色搭配和视觉提示,同时遵循Material Design规范。探

探索页是教育百科App的内容导航中心,用户可以在这里找到所有功能模块的入口。说白了,这个页面就是一个"功能大全",把App里能做的事情都列出来,让用户自己挑。
做这个页面的时候我一直在想一个问题:怎么把这么多功能入口组织得清晰又不无聊?最后我选择了分类卡片的方式,把相关的功能放在一起,用不同的颜色区分不同的类别。效果还不错,下面来看看具体实现。
为什么用StatelessWidget
探索页和首页不一样,它不需要从网络加载数据,所有内容都是写死的:
class ExploreTab extends StatelessWidget {
const ExploreTab({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(‘探索’),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSearchCard(context),
const SizedBox(height: 24),
// 各个分类模块…
],
),
);
}
}
既然没有状态需要管理,那就用StatelessWidget,简单直接。ListView的padding设置为16,让内容和屏幕边缘保持一定距离,看起来不会太挤。
有人可能会问:为什么不用SingleChildScrollView?
其实都可以,但ListView有个好处——它是懒加载的。虽然探索页的内容不多,懒加载的优势体现不出来,但养成用ListView的习惯是好的,以后内容多了也不用改。
搜索入口卡片
页面最顶部是一个搜索入口,点击后跳转到图书搜索页面:
Widget buildSearchCard(BuildContext context) {
return Card(
child: InkWell(
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: () => const BookSearchScreen())),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.search,
color: Theme.of(context).colorScheme.primary),
),
这里用Card包裹InkWell,而不是直接用InkWell,是因为Card自带圆角和阴影效果,省得自己写。InkWell的borderRadius要和Card的圆角一致,否则点击时的水波纹会超出边界,很难看。
关于颜色的选择
搜索图标的背景色用的是primaryContainer,图标本身用的是primary。这是Material Design 3推荐的做法,好处是会自动适配主题色,不管用户选什么主题,颜色搭配都是协调的。
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('搜索知识',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
Text('搜索图书、国家、大学等',
style: TextStyle(color: Colors.grey[600])),
],
),
),
const Icon(Icons.arrow_forward_ios, size: 16),
],
),
),
),
);
}
右侧的小箭头是个视觉提示,告诉用户这个卡片可以点击。箭头用16的尺寸,不要太大,否则会抢了主要内容的风头。
分类模块的封装
探索页有好几个分类:图书馆、世界地理、高等教育、百科知识等等。每个分类的样式都一样,只是内容不同,所以我把它封装成了一个方法:
Widget _buildCategorySection(BuildContext context, String title,
IconData icon, Color color, List<_CategoryItem> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Text(title, style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
],
),
分类标题前面有一个带颜色的图标,图标的背景色是主色的10%透明度版本。这个设计挺常见的,既能突出图标,又不会太刺眼。
const SizedBox(height: 12),
...items.map((item) => Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(item.icon, color: color),
title: Text(item.title),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: item.onTap,
),
)),
],
);
}
每个功能项用ListTile展示,leading放图标,title放标题,trailing放箭头。ListTile是Flutter里做列表项的标准组件,自带点击效果和合适的间距,用起来很省心。
…items.map这个写法
这是Dart的展开运算符,把map返回的Iterable展开成多个Widget。如果不用展开运算符,就得先toList(),然后用addAll,麻烦得多。
功能项的数据结构
为了方便管理功能项的数据,我定义了一个简单的类:
class _CategoryItem {
final String title;
final IconData icon;
final VoidCallback onTap;
_CategoryItem(this.title, this.icon, this.onTap);
}
类名前面的下划线表示这是私有的,只在当前文件里用。这种做法让代码更模块化,每个功能项的标题、图标和点击事件都封装在一起,一目了然。
为什么不直接用Map?
用Map也行,但是类型不安全。比如你写错了key名,编译器不会报错,运行时才会出问题。用类的话,属性名写错了编译器立刻就会提示。
各分类的具体配置
来看看图书馆分类是怎么配置的:
_buildCategorySection(context, ‘图书馆’, Icons.menu_book, Colors.blue, [
CategoryItem(‘搜索图书’, Icons.search,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const BookSearchScreen()))),
CategoryItem(‘热门图书’, Icons.trending_up,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const BookListScreen()))),
]),
图书馆用蓝色,包含两个功能:搜索图书和热门图书。每个功能项的onTap都是一个Navigator.push,跳转到对应的页面。
世界地理分类
_buildCategorySection(context, ‘世界地理’, Icons.public, Colors.green, [
CategoryItem(‘所有国家’, Icons.flag,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const CountryListScreen()))),
CategoryItem(‘按地区浏览’, Icons.map,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const CountryByRegionScreen()))),
]),
世界地理用绿色,提供两种浏览方式:查看所有国家,或者按地区(亚洲、欧洲、非洲等)浏览。这两种方式满足不同用户的需求,有人喜欢看全部,有人喜欢按分类找。
高等教育分类
_buildCategorySection(context, ‘高等教育’, Icons.school, Colors.orange, [
CategoryItem(‘搜索大学’, Icons.search,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const UniversityListScreen()))),
]),
高等教育用橙色,目前只有一个功能:搜索大学。后续可以加更多功能,比如大学排名、专业介绍什么的。
百科知识分类
_buildCategorySection(context, ‘百科知识’, Icons.auto_stories, Colors.purple, [
CategoryItem(‘维基百科’, Icons.article,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const WikipediaScreen()))),
CategoryItem(‘历史上的今天’, Icons.today,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const OnThisDayScreen()))),
]),
百科知识用紫色,包含维基百科搜索和历史上的今天两个功能。历史上的今天是个挺有意思的功能,每天都能看到不同的历史事件。
数学趣闻分类
_buildCategorySection(context, ‘数学趣闻’, Icons.calculate, Colors.teal, [
CategoryItem(‘数字趣闻’, Icons.tag,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const NumbersScreen()))),
]),
数学趣闻用青色(teal),目前只有数字趣闻一个功能。输入任意数字,就能看到关于这个数字的有趣知识。
知识问答分类
_buildCategorySection(context, ‘知识问答’, Icons.quiz, Colors.red, [
CategoryItem(‘开始答题’, Icons.play_arrow,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const QuizScreen()))),
CategoryItem(‘题目分类’, Icons.category,
() => Navigator.push(context,
MaterialPageRoute(builder: () => const QuizCategoriesScreen()))),
]),
知识问答用红色,包含快速开始答题和按分类选择题目两个入口。红色比较醒目,适合这种互动性强的功能。
颜色的选择逻辑
你可能注意到了,每个分类用的颜色都不一样。这不是随便选的,而是有一定逻辑的:
- 蓝色给图书 - 蓝色给人沉稳、知性的感觉,和阅读很搭
- 绿色给地理 - 绿色让人联想到地球、自然,和地理主题呼应
- 橙色给教育 - 橙色活泼、积极,适合教育相关的内容
- 紫色给百科 - 紫色有种神秘、深邃的感觉,和知识探索很配
- 青色给数学 - 青色冷静、理性,符合数学的气质
- 红色给问答 - 红色热情、刺激,适合竞技性的答题功能
这种颜色编码能帮助用户快速识别不同的功能区域,形成视觉记忆。
页面导航的统一处理
所有功能项点击后都会跳转到对应的详情页面,用的是最基本的Navigator.push:
Navigator.push(context,
MaterialPageRoute(builder: (_) => const WikipediaScreen()))
MaterialPageRoute会自动处理页面切换动画,在iOS上是从右向左滑入,在Android上是从下向上淡入。这种平台适配是Flutter自动完成的,不需要写额外的代码。
为什么不用命名路由?
命名路由(Named Routes)在大型项目里确实更好管理,但对于这个App来说,直接push就够了。命名路由需要在MaterialApp里统一注册,配置起来麻烦一些,而且传参也没有直接push方便。
底部留白的处理
在ListView的最后,我加了一个SizedBox来留出底部空间:
const SizedBox(height: 100),
这100像素的留白是为了避免底部导航栏遮挡内容。当用户滚动到页面底部时,最后一个分类模块能完整显示出来,不会被TabBar挡住一部分。
这个数值怎么定的?
100是我试出来的。底部导航栏的高度大概是56-80像素,再加上一些安全边距,100差不多刚好。当然,更严谨的做法是用MediaQuery获取底部安全区域的高度,但对于这个场景来说,写死100也够用了。
关于页面性能
探索页的内容都是静态的,没有网络请求,所以性能完全不是问题。但有几个小细节还是值得注意:
1. 避免在build里创建对象
// 不好的写法
Widget build(BuildContext context) {
final items = [_CategoryItem(…), _CategoryItem(…)]; // 每次build都创建新对象
return …;
}
// 好的写法
static final _items = [_CategoryItem(…), _CategoryItem(…)]; // 只创建一次
Widget build(BuildContext context) {
return …;
}
虽然对于这个页面来说影响不大,但养成好习惯总是对的。
2. const构造函数
能用const的地方尽量用const,比如const SizedBox(height: 24)。const对象在编译时就创建好了,运行时不需要重复创建,能省一点内存。
后续扩展的思考
探索页目前的设计是比较简单的,如果以后要加更多功能,可以考虑这几个方向:
- 搜索功能 - 在顶部加一个搜索框,可以搜索所有功能
- 最近使用 - 记录用户最近访问的功能,放在页面顶部
- 个性化推荐 - 根据用户的使用习惯,推荐可能感兴趣的功能
- 分类折叠 - 如果分类太多,可以做成可折叠的,默认只显示标题
不过目前的功能数量还不多,这些优化暂时用不上。等功能多了再说吧。
小结
探索页的实现相对简单,主要是静态内容的展示和页面导航。通过合理的分类组织和统一的视觉样式,用户可以快速找到想要的功能。颜色编码和图标的运用让页面更加直观易懂,不需要看文字就能大概知道每个分类是干什么的。
下一篇我们来看问答模块的实现,那里会有更多的交互逻辑和状态管理。
本文是Flutter for OpenHarmony教育百科实战系列的第二篇。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)