Flutter for OpenHarmony PUBG游戏助手App实战:队友匹配系统
组建一个好的队伍对游戏成功很重要。这个模块我一般会先把“可用队友列表 + 选择 + 邀请反馈”做成一个最小闭环,跑通之后再慢慢加细节(上限、空态、可维护性拆分)。下面的实现会尽量按真实项目写法去组织:每段代码都不长,并且每段后面紧跟一段说明,方便你直接搬到工程里改。
·

组建一个好的队伍对游戏成功很重要。这个模块我一般会先把“可用队友列表 + 选择 + 邀请反馈”做成一个最小闭环,跑通之后再慢慢加细节(上限、空态、可维护性拆分)。
下面的实现会尽量按真实项目写法去组织:每段代码都不长,并且每段后面紧跟一段说明,方便你直接搬到工程里改。
效果预览

这一张图能看出两个关键点
- 入口明确:首页“队友匹配”是一个独立功能卡片。
- 风格统一:深色背景 + 高亮按钮颜色,这类 UI 在游戏工具 App 里比较常见。
队友数据模型(先把字段定死)
class Player {
final String name;
final int level;
final double winRate;
final double kd;
final String role;
final String status;
const Player({
required this.name,
required this.level,
required this.winRate,
required this.kd,
required this.role,
required this.status,
});
}
这段模型主要在干什么
- 字段集中:页面展示要用到的信息,模型里先收敛好,后面换数据源(接口/缓存)都更顺。
- 类型固定:
winRate、kd用double,避免 UI 层再做乱七八糟的转换。
mock 数据(先让页面跑起来)
class TeamMatcher {
static final List<Player> availablePlayers = [
Player(
name: '职业选手',
level: 50,
winRate: 35.5,
kd: 4.2,
role: '突击手',
status: '在线',
),
Player(
name: '狙击高手',
level: 48,
winRate: 28.3,
kd: 3.8,
role: '狙击手',
status: '在线',
),
];
}
为什么我倾向于单独放一个“数据提供者”
- 页面更干净:UI 不直接 new 数据,后期你把它替换成接口调用时,改动面更小。
- 便于做筛选:比如“只看在线”“按定位过滤”,都可以先在这里处理。
页面骨架(先拆出列表和底部操作区)
class TeamMatcherPage extends StatefulWidget {
const TeamMatcherPage({Key? key}) : super(key: key);
State<TeamMatcherPage> createState() => _TeamMatcherPageState();
}
这里没什么花活
- 保持简单:这个页面内部就能完成选择状态维护,先不引入额外状态管理,便于你理解。
class _TeamMatcherPageState extends State<TeamMatcherPage> {
static const int _maxSelect = 3;
final Set<int> _selectedPlayers = <int>{};
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('队友匹配'),
backgroundColor: const Color(0xFF2D2D2D),
),
backgroundColor: const Color(0xFF1A1A1A),
body: Column(
children: [
Expanded(child: _buildPlayerList()),
_buildInviteBar(),
],
),
);
}
}
骨架这么拆的好处
- 结构稳:列表永远占满剩余高度,底部按钮区按需显示。
- 后期易加功能:比如加搜索框、筛选条,只需要往
Column里插一段即可。
列表渲染(只负责把数据变成卡片)
Widget _buildPlayerList() {
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: TeamMatcher.availablePlayers.length,
itemBuilder: (context, index) {
return _buildPlayerCard(TeamMatcher.availablePlayers[index], index);
},
);
}
这段刻意“很薄”
- 不写业务:只做循环渲染,不在
itemBuilder里塞选择逻辑。 - 方便替换数据源:以后换成分页、下拉刷新也好改。
选择逻辑(带上限 + 反馈)
void _toggleSelect(int index) {
final isSelected = _selectedPlayers.contains(index);
if (!isSelected && _selectedPlayers.length >= _maxSelect) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('最多只能选择 3 位队友')),
);
return;
}
setState(() {
if (isSelected) {
_selectedPlayers.remove(index);
} else {
_selectedPlayers.add(index);
}
});
}
真实项目里这个点很常见
- 上限约束要有提示:点了没反应是最差体验,
SnackBar至少让用户知道原因。 - 集合比 List 合适:
Set<int>天然去重,不怕重复选中。
队友卡片(信息展示 + 右侧操作)
Widget _buildPlayerCard(Player player, int index) {
final isSelected = _selectedPlayers.contains(index);
return Card(
margin: EdgeInsets.only(bottom: 12.h),
color: const Color(0xFF2D2D2D),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
player.name,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
_buildRoleTag(player.role),
],
),
SizedBox(height: 8.h),
Text(
'等级: ${player.level} 胜率: ${player.winRate}% K/D: ${player.kd}',
style: TextStyle(color: Colors.white70, fontSize: 11.sp),
),
],
),
),
_buildSelectButton(isSelected, () => _toggleSelect(index)),
],
),
),
);
}
为什么把“操作”放到右侧
- 滑动更稳定:列表滑动时整卡可点击容易误触;右侧按钮区更明确。
- 状态更直观:按钮变色 + 图标变化,用户几乎不用读文字。
角色标签(小组件,小而有用)
Widget _buildRoleTag(String role) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
decoration: BoxDecoration(
color: const Color(0xFF4CAF50).withOpacity(0.2),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
role,
style: TextStyle(
color: const Color(0xFF4CAF50),
fontSize: 10.sp,
fontWeight: FontWeight.bold,
),
),
);
}
这段属于“看起来小,但很值”的封装
- 统一风格:标签样式以后想改只改一个地方。
- 减少嵌套:卡片内部少几层
Container,读起来轻松很多。
选择按钮(封装点击区域与视觉状态)
Widget _buildSelectButton(bool isSelected, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFFF6B35) : Colors.white10,
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(
isSelected ? Icons.check : Icons.add,
color: Colors.white,
size: 20.sp,
),
),
);
}
这里我一般会关注两件事
- 触达面积:40x40 基本够用,太小会难点。
- 状态反馈:颜色 + 图标同时变化,弱光环境也看得清。
邀请按钮区(不选就不显示)
Widget _buildInviteBar() {
if (_selectedPlayers.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: EdgeInsets.all(16.w),
child: ElevatedButton(
onPressed: _inviteSelected,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF6B35),
minimumSize: Size(double.infinity, 50.h),
),
child: Text(
'邀请 ${_selectedPlayers.length} 位队友',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
),
);
}
这种写法比较省心
- 无选择时不占位置:
SizedBox.shrink()不会撑起高度。 - 文案带数量:用户知道自己当前选了几个,不用再看别处。
邀请动作(先做反馈,后面再接接口)
void _inviteSelected() {
final names = _selectedPlayers
.map((i) => TeamMatcher.availablePlayers[i].name)
.join('、');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已向 $names 发送邀请')),
);
}
为什么我不把逻辑直接写进 onPressed
- 后面好扩展:接后端接口时要加 loading、失败提示、重试,都在这里改就行。
- 页面更干净:UI 代码只负责“按下去调用什么”,不要夹杂业务细节。
小结
这个队友匹配页面做完,你会发现真正影响体验的不是控件有多少,而是这些细节:
- 选择有上限也有提示:规则明确,用户就不会迷糊。
- 代码结构能拆就拆:列表、卡片、按钮区分开,后面加功能不痛苦。
- 先闭环再优化:先让“选人—邀请—反馈”跑通,之后再加筛选、搜索、接口接入。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)