Flutter for OpenHarmony社团管理App实战:社团排行榜实现
本文介绍了如何实现一个社团排行榜页面,主要功能包括:按评分降序展示社团列表、为前三名添加特殊标识,以及点击跳转至社团详情页。技术实现上使用Flutter框架,通过Provider进行状态管理,ListView.builder构建高效列表。页面布局采用Card组件展示各社团信息,包含排名徽章、首字母头像、社团名称、分类标签和成员数量等元素,前三名通过颜色和图标进行视觉区分。点击交互使用InkWell

社团之间总是有点良性竞争的,谁的活动办得好、谁的成员多、谁的评分高,大家都想知道。今天我们就来实现一个社团排行榜页面,让各社团的实力一目了然。
这个页面要做的事情:按评分排序展示社团、前三名要有特殊标识、点击能跳转到社团详情。
引入依赖
import 'package:flutter/material.dart';
Material库提供基础UI组件,每个页面都要引入。
没它啥也干不了。
import 'package:provider/provider.dart';
Provider做状态管理,社团数据从这里拿。
数据变了页面自动刷新。
import '../../providers/app_provider.dart';
我们自己的Provider,存着社团列表。
路径根据项目结构调整。
import 'club_detail_page.dart';
社团详情页面,点击排行榜项目跳转过去。
让用户能看到更多信息。
页面基本结构
class ClubRankingPage extends StatelessWidget {
const ClubRankingPage({super.key});
用StatelessWidget就行,不需要维护状态。
数据都从Provider拿。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('社团排行榜')
),
Scaffold搭架子,AppBar放标题。
标准套路。
获取并排序数据
body: Consumer<AppProvider>(
builder: (context, provider, _) {
final clubs = List.of(provider.clubs)
..sort((a, b) => b.rating.compareTo(a.rating));
Consumer监听Provider,数据变了自动重建。
List.of创建副本避免修改原数据,sort按评分降序排列。
构建列表
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: clubs.length,
itemBuilder: (context, index) {
final club = clubs[index];
final rank = index + 1;
ListView.builder渲染列表,性能好。
rank是排名,从1开始。
排行卡片容器
return Card(
margin: const EdgeInsets.only(
bottom: 12
),
child: InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ClubDetailPage(
club: club
)
)
),
Card包裹卡片,InkWell提供点击效果。
点击跳转到社团详情页。
卡片内容布局
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildRankBadge(rank),
const SizedBox(width: 16),
Row横向排列:排名徽章、头像、信息、评分。
SizedBox加间距。
社团头像
CircleAvatar(
radius: 24,
backgroundColor: const Color(0xFF4A90E2).withOpacity(0.1),
child: Text(
club.name[0],
style: const TextStyle(
color: Color(0xFF4A90E2),
fontWeight: FontWeight.bold
)
),
),
const SizedBox(width: 12),
头像显示社团名首字母,蓝色主题。
没有真实头像时常用这种方式。
社团信息区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
club.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16
)
),
const SizedBox(height: 4),
Expanded让信息区占满剩余空间。
社团名用粗体突出。
分类和成员数
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2
),
decoration: BoxDecoration(
color: const Color(0xFF4A90E2).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
club.category,
style: const TextStyle(
color: Color(0xFF4A90E2),
fontSize: 11
)
),
),
分类标签用蓝色小字,背景浅蓝。
让用户知道这是什么类型的社团。
const SizedBox(width: 8),
const Icon(
Icons.people,
size: 14,
color: Colors.grey
),
const SizedBox(width: 4),
Text(
'${club.memberCount}人',
style: const TextStyle(
fontSize: 12,
color: Colors.grey
)
),
],
),
],
),
),
成员数用人物图标配合数字显示。
灰色表示次要信息。
评分显示
Column(
children: [
Row(
children: [
const Icon(
Icons.star,
color: Colors.amber,
size: 20
),
const SizedBox(width: 4),
Text(
club.rating.toStringAsFixed(1),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Color(0xFF4A90E2)
)
),
],
),
星星图标配合评分数字,金色星星很醒目。
评分保留一位小数。
const Text(
'评分',
style: TextStyle(
fontSize: 12,
color: Colors.grey
)
),
],
),
],
),
),
),
);
},
);
},
),
);
}
评分下面加个标签说明。
整个卡片结构就这样。
排名徽章组件
Widget _buildRankBadge(int rank) {
Color color;
IconData? icon;
switch (rank) {
case 1:
color = const Color(0xFFFFD700);
icon = Icons.emoji_events;
break;
第一名用金色,显示奖杯图标。
金色是冠军的标志。
case 2:
color = const Color(0xFFC0C0C0);
icon = Icons.emoji_events;
break;
第二名用银色,也显示奖杯。
银色代表亚军。
case 3:
color = const Color(0xFFCD7F32);
icon = Icons.emoji_events;
break;
第三名用铜色,同样显示奖杯。
铜色代表季军。
default:
color = Colors.grey;
icon = null;
}
其他名次用灰色,不显示图标。
只显示数字就行。
徽章容器
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withOpacity(0.2),
shape: BoxShape.circle,
),
圆形容器,背景是主色调的浅色版。
大小固定36x36。
child: Center(
child: icon != null
? Icon(
icon,
color: color,
size: 20
)
: Text(
'$rank',
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 16
)
),
),
);
}
}
前三名显示奖杯图标,其他显示数字。
条件表达式控制显示内容。
社团数据模型
class Club {
final String id;
final String name;
final String category;
final int memberCount;
final double rating;
final String description;
Club({
required this.id,
required this.name,
required this.category,
required this.memberCount,
required this.rating,
required this.description,
});
}
社团模型包含这些字段。
rating是评分,用于排序。
Provider中的社团数据
class AppProvider extends ChangeNotifier {
List<Club> _clubs = [];
List<Club> get clubs => _clubs;
void setClubs(List<Club> clubs) {
_clubs = clubs;
notifyListeners();
}
}
Provider存着社团列表。
setClubs更新数据后通知监听者。
测试数据
void initTestClubs(AppProvider provider) {
provider.setClubs([
Club(
id: 'club001',
name: '编程爱好者协会',
category: '科技',
memberCount: 128,
rating: 4.8,
description: '热爱编程的同学聚集地',
),
开发阶段用测试数据调试。
这个社团评分4.8,应该排第一。
Club(
id: 'club002',
name: '摄影社',
category: '艺术',
memberCount: 86,
rating: 4.6,
description: '用镜头记录美好瞬间',
),
摄影社评分4.6,排第二。
不同分类的社团都有。
Club(
id: 'club003',
name: '篮球社',
category: '体育',
memberCount: 156,
rating: 4.5,
description: '热爱篮球运动的大家庭',
),
篮球社评分4.5,排第三。
成员数最多但评分不是最高。
Club(
id: 'club004',
name: '读书会',
category: '文化',
memberCount: 62,
rating: 4.3,
description: '分享阅读的快乐',
),
]);
}
读书会排第四,显示数字4。
没有奖杯图标了。
页面路由
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ClubRankingPage()
),
);
从其他页面跳转过来就这么写。
标准路由跳转。
在发现页添加入口
ListTile(
leading: const Icon(
Icons.leaderboard,
color: Color(0xFF4A90E2)
),
title: const Text('社团排行榜'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ClubRankingPage()
),
);
},
),
发现页放个入口,点击跳转。
leaderboard图标很形象。
关于排序方式
// 按评分排序
clubs.sort((a, b) => b.rating.compareTo(a.rating));
// 按成员数排序
clubs.sort((a, b) => b.memberCount.compareTo(a.memberCount));
// 按名称排序
clubs.sort((a, b) => a.name.compareTo(b.name));
可以支持多种排序方式。
让用户选择按什么排序。
添加排序切换
enum SortType { rating, memberCount, name }
class ClubRankingPage extends StatefulWidget {
// ...
}
class _ClubRankingPageState extends State<ClubRankingPage> {
SortType _sortType = SortType.rating;
void _changeSortType(SortType type) {
setState(() {
_sortType = type;
});
}
}
用枚举定义排序类型。
切换时调用setState刷新。
排序按钮
PopupMenuButton<SortType>(
icon: const Icon(Icons.sort),
onSelected: _changeSortType,
itemBuilder: (context) => [
const PopupMenuItem(
value: SortType.rating,
child: Text('按评分')
),
const PopupMenuItem(
value: SortType.memberCount,
child: Text('按人数')
),
const PopupMenuItem(
value: SortType.name,
child: Text('按名称')
),
],
)
PopupMenuButton做排序切换。
放在AppBar的actions里。
根据类型排序
List<Club> _sortClubs(List<Club> clubs) {
final sorted = List.of(clubs);
switch (_sortType) {
case SortType.rating:
sorted.sort((a, b) => b.rating.compareTo(a.rating));
break;
case SortType.memberCount:
sorted.sort((a, b) => b.memberCount.compareTo(a.memberCount));
break;
case SortType.name:
sorted.sort((a, b) => a.name.compareTo(b.name));
break;
}
return sorted;
}
根据当前排序类型排序。
返回排序后的列表。
关于颜色常量
const Color goldColor = Color(0xFFFFD700);
const Color silverColor = Color(0xFFC0C0C0);
const Color bronzeColor = Color(0xFFCD7F32);
金银铜三种颜色定义成常量。
方便统一管理和复用。
小结
社团排行榜页面实现起来不复杂,核心就是排序和展示。前三名用金银铜奖杯突出显示,其他名次用数字,视觉效果清晰。
代码里用到的技巧:List.sort排序、switch条件判断、条件表达式控制显示内容。这些在其他场景也经常用到。
实际项目中可能还要考虑分页加载、筛选条件这些,但基本框架就是这样。把核心功能做好,其他的慢慢加。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)