剧本杀作为一种新兴的社交娱乐方式,近年来在国内迅速流行。作为一款专为剧本杀爱好者设计的组队App,游戏记录功能是用户留存和体验提升的关键模块。这个功能不仅能让玩家回顾自己的游戏历程,还能为未来的剧本选择提供参考依据。
在Flutter for OpenHarmony环境下实现这一功能,我们面临着跨平台适配和性能优化的双重挑战。游戏记录页面需要展示丰富的信息,包括剧本名称、游戏日期、扮演角色、个人评分等,同时还要支持筛选、排序和分享等交互功能。
游戏记录页面的核心功能可以分为以下几个部分:
在技术实现上,我们选择了以下方案:
这种组合在保证开发效率的同时,也能满足OpenHarmony平台的性能要求。GetX的轻量级特性特别适合移动端应用,避免了状态管理带来的性能开销。
游戏记录的数据结构设计至关重要,我们采用了Map<String, dynamic>类型来存储单条记录:
dart复制{
'id': '1', // 唯一标识
'script': '年轮', // 剧本名称
'type': '情感本', // 剧本类型
'store': '迷雾剧本杀', // 游戏店铺
'date': '2024-01-15', // 游戏日期
'rating': 5, // 用户评分(1-5星)
'role': '凶手', // 扮演角色
'duration': '120分钟', // 游戏时长
'players': 6, // 参与人数
'comment': '非常精彩的剧本,剧情紧凑,反转精彩', // 用户评论
}
这种设计具有以下优点:
页面整体采用Column+ListView的经典布局:
dart复制Scaffold(
appBar: AppBar(
title: const Text('游戏记录'),
actions: [FilterButton()],
),
body: Column(
children: [
StatisticsBar(), // 统计信息栏
Expanded(
child: ListView.builder(
itemCount: records.length,
itemBuilder: (ctx, index) => RecordItem(records[index]),
),
),
],
),
)
这种布局方式确保了:
统计栏展示三个关键指标:
dart复制int totalGames = records.length;
double avgRating = records.isEmpty ? 0 :
records.fold(0, (sum, item) => sum + item['rating']) / totalGames;
String favoriteRole = _calculateFavoriteRole();
计算最爱角色的方法:
dart复制String _calculateFavoriteRole() {
final roleCount = <String, int>{};
for (var record in records) {
final role = record['role'];
roleCount[role] = (roleCount[role] ?? 0) + 1;
}
return roleCount.entries.reduce((a, b) => a.value > b.value ? a : b).key;
}
筛选菜单使用GetX的bottomSheet实现:
dart复制void _showFilterMenu() {
Get.bottomSheet(
Container(
child: Column(
children: [
Text('时间筛选'),
Wrap(
children: ['全部', '本月', '本年', '去年'].map((filter) {
return ChoiceChip(
label: Text(filter),
selected: _selectedFilter == filter,
onSelected: (_) => _applyFilter(filter),
);
}).toList(),
),
// 排序选项类似
],
),
),
);
}
筛选逻辑实现:
dart复制void _applyFilter(String filter) {
List<Map<String, dynamic>> filtered = [..._allRecords];
// 时间筛选
if (filter != '全部') {
final now = DateTime.now();
filtered = filtered.where((record) {
final date = DateTime.parse(record['date']);
switch (filter) {
case '本月':
return date.month == now.month && date.year == now.year;
case '本年':
return date.year == now.year;
case '去年':
return date.year == now.year - 1;
default:
return true;
}
}).toList();
}
// 排序逻辑
filtered.sort((a, b) {
switch (_sortBy) {
case '最新':
return b['date'].compareTo(a['date']);
case '最旧':
return a['date'].compareTo(b['date']);
case '评分最高':
return b['rating'].compareTo(a['rating']);
case '评分最低':
return a['rating'].compareTo(b['rating']);
default:
return 0;
}
});
setState(() => _records = filtered);
}
点击记录项跳转到详情页:
dart复制GestureDetector(
onTap: () => Get.to(() => ScriptDetailPage(scriptId: record['id'])),
child: RecordItemWidget(record),
)
单个游戏记录卡片采用多层次信息展示:
dart复制Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 6)],
),
child: Column(
children: [
// 剧本名称和角色标签
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(record['script'], style: TextStyle(fontWeight: FontWeight.bold)),
Text(record['type'], style: TextStyle(color: Colors.grey)),
],
),
),
RoleTag(role: record['role']),
],
),
// 店铺和日期信息
Row(
children: [
Icon(Icons.store, size: 14),
Text(record['store']),
Spacer(),
Icon(Icons.calendar_today, size: 14),
Text(record['date']),
],
),
// 评分和人数
Row(
children: [
Text('我的评分:'),
StarRating(rating: record['rating']),
Spacer(),
Text('${record['players']}人'),
],
),
// 评论(如果有)
if (record['comment'] != null && record['comment'].isNotEmpty)
CommentBox(comment: record['comment']),
],
),
)
自定义星级评分组件:
dart复制class StarRating extends StatelessWidget {
final int rating;
StarRating({required this.rating});
@override
Widget build(BuildContext context) {
return Row(
children: List.generate(5, (index) {
return Icon(
index < rating ? Icons.star : Icons.star_border,
size: 16,
color: Colors.amber,
);
}),
);
}
}
对于可能很长的游戏记录列表,我们采取了以下优化措施:
dart复制ListView.builder(
itemCount: records.length,
itemBuilder: (context, index) {
return RecordItem(
key: ValueKey(records[index]['id']),
record: records[index],
);
},
)
如果记录中包含剧本封面图片:
dart复制CachedNetworkImage(
imageUrl: record['coverUrl'],
placeholder: (ctx, url) => Placeholder(),
errorWidget: (ctx, url, err) => Icon(Icons.error),
fadeInDuration: Duration(milliseconds: 300),
)
针对游戏记录页面的关键测试场景:
dart复制test('测试筛选逻辑', () {
// 准备测试数据
final testRecords = [...];
final page = HistoryPage();
// 测试时间筛选
page._applyFilter('本月');
expect(page._records.length, equals(2));
// 测试排序
page._sortBy = '评分最高';
page._applyFilter('全部');
expect(page._records.first['rating'], equals(5));
});
主要测试用户交互流程:
dart复制testWidgets('测试完整用户流程', (tester) async {
await tester.pumpWidget(MyApp());
// 1. 进入游戏记录页
await tester.tap(find.text('游戏记录'));
await tester.pumpAndSettle();
// 2. 点击筛选按钮
await tester.tap(find.byIcon(Icons.filter_list));
await tester.pumpAndSettle();
// 3. 选择"本月"筛选
await tester.tap(find.text('本月'));
await tester.pumpAndSettle();
// 验证筛选结果
expect(find.text('年轮'), findsOneWidget);
expect(find.text('古木吟'), findsNothing);
});
将游戏记录保存到本地数据库:
dart复制class RecordDatabase {
static final RecordDatabase _instance = RecordDatabase._internal();
factory RecordDatabase() => _instance;
late Database _db;
Future<void> init() async {
_db = await openDatabase(
'records.db',
version: 1,
onCreate: (db, version) {
return db.execute('''
CREATE TABLE records(
id TEXT PRIMARY KEY,
script TEXT,
type TEXT,
store TEXT,
date TEXT,
rating INTEGER,
role TEXT,
duration TEXT,
players INTEGER,
comment TEXT
)
''');
},
);
}
Future<List<Map<String, dynamic>>> getRecords() async {
return await _db.query('records');
}
Future<void> addRecord(Map<String, dynamic> record) async {
await _db.insert('records', record);
}
}
集成社交平台分享SDK:
dart复制void _shareRecord(Map<String, dynamic> record) async {
final shareText = '''
我在${record['store']}玩了《${record['script']}》!
扮演角色:${record['role']}
我的评分:${'⭐' * record['rating']}
${record['comment'] ?? ''}
''';
await Share.share(shareText);
}
当从网络获取最新记录时,需要注意:
dart复制Future<void> _loadRecords() async {
setState(() => _isLoading = true);
try {
final remoteRecords = await Api.getRecords();
final localRecords = await RecordDatabase().getRecords();
// 合并并去重
final allRecords = {...localRecords, ...remoteRecords}.values.toList();
setState(() {
_allRecords = allRecords;
_records = allRecords;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar('错误', '加载记录失败: ${e.toString()}');
}
}
当没有游戏记录时显示友好提示:
dart复制Widget _buildContent() {
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
if (_records.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 80, color: Colors.grey[300]),
Text('还没有游戏记录'),
TextButton(
onPressed: _goToLobby,
child: Text('去组队大厅'),
),
],
),
);
}
return _buildRecordList();
}
在OpenHarmony平台上需要注意:
dart复制// 在main.dart中初始化
void main() {
if (Platform.isOpenHarmony) {
initOpenHarmony();
}
runApp(MyApp());
}
添加性能监控代码:
dart复制void _monitorPerformance() {
FlutterFPSMonitor().start();
// 记录关键操作耗时
final stopwatch = Stopwatch()..start();
_applyFilter('本月');
stopwatch.stop();
Analytics().logEvent('filter_time', {
'duration': stopwatch.elapsedMilliseconds,
'filter': '本月',
});
}
在实际开发中,我发现游戏记录页面的性能瓶颈主要出现在大量数据渲染时。通过以下几点优化可以显著提升体验:
另一个值得注意的点是数据同步策略。在移动网络环境下,频繁同步大量游戏记录会消耗用户流量并影响性能。我们最终采用的方案是:
这种策略在保证数据及时性的同时,也兼顾了性能和流量消耗的平衡。