免费游戏是游戏库App的核心内容。与宝可梦图鉴不同,游戏列表需要支持多维度的筛选:按平台(PC、浏览器)、按分类(MMORPG、射击、策略等)。这篇文章我们来实现一个功能完整的免费游戏列表页面,包括Tab切换、分类筛选、列表展示等功能。
请添加图片描述

页面的整体架构

首先看看页面的设计。免费游戏列表分为两个维度的筛选:

class FreeGamesScreen extends StatefulWidget {
  const FreeGamesScreen({super.key});

  
  State<FreeGamesScreen> createState() => _FreeGamesScreenState();
}

class _FreeGamesScreenState extends State<FreeGamesScreen> with SingleTickerProviderStateMixin {
  final FreeToGameApi _api = FreeToGameApi();
  late TabController _tabController;
  List<dynamic> _allGames = [];
  List<dynamic> _pcGames = [];
  List<dynamic> _browserGames = [];
  bool _isLoading = true;
  String _selectedCategory = 'all';

  final List<String> _categories = ['all', 'mmorpg', 'shooter', 'strategy', 'moba', 'racing', 'sports', 'social', 'card'];
  final Map<String, String> _categoryNames = {
    'all': '全部',
    'mmorpg': 'MMORPG',
    'shooter': '射击',
    'strategy': '策略',
    'moba': 'MOBA',
    'racing': '竞速',
    'sports': '体育',
    'social': '社交',
    'card': '卡牌',
  };

这个页面用SingleTickerProviderStateMixin来支持TabController。_tabController管理三个Tab:全部、PC、浏览器。

_selectedCategory记录当前选中的分类。_categories列表定义了所有可用的分类,_categoryNames是分类的中文名称映射。

TabController的初始化

initState中初始化TabController:

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _loadGames();
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

TabController需要指定length(Tab数量)和vsync(通常是this)。vsync用来同步动画,确保Tab切换时的动画流畅。

在dispose中一定要释放TabController,否则会导致内存泄漏

多维度的数据加载

_loadGames方法根据选中的分类和平台加载游戏:

  Future<void> _loadGames() async {
    setState(() => _isLoading = true);
    try {
      final category = _selectedCategory == 'all' ? null : _selectedCategory;
      final all = await _api.getGameList(category: category);
      final pc = await _api.getGameList(platform: 'pc', category: category);
      final browser = await _api.getGameList(platform: 'browser', category: category);
      setState(() {
        _allGames = all;
        _pcGames = pc;
        _browserGames = browser;
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
    }
  }

这个方法并行加载三个API请求:全部游戏、PC游戏、浏览器游戏。每个请求都可以指定分类。

当_selectedCategory为’all’时,category参数为null,这样API会返回所有分类的游戏。

三个请求是串行的(一个接一个),如果要提升性能可以用Future.wait并行加载。

页面的UI结构

页面分为三部分:AppBar、分类筛选、游戏列表:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('免费游戏'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '全部'),
            Tab(text: 'PC'),
            Tab(text: '浏览器'),
          ],
        ),
      ),

AppBar的bottom是一个TabBar,显示三个Tab。TabBar会自动处理Tab之间的切换。

分类筛选的实现:

      body: Column(
        children: [
          SizedBox(
            height: 50,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
              itemCount: _categories.length,
              itemBuilder: (context, index) {
                final cat = _categories[index];
                return Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 4),
                  child: FilterChip(
                    label: Text(_categoryNames[cat] ?? cat),
                    selected: _selectedCategory == cat,
                    onSelected: (selected) {
                      setState(() => _selectedCategory = cat);
                      _loadGames();
                    },
                  ),
                );
              },
            ),
          ),

分类筛选用横向的ListView来展示。每个分类是一个FilterChip,用户可以点击来选择。

FilterChip的selected属性控制是否被选中。当用户选择一个分类时,更新_selectedCategory并重新加载游戏。

这个设计很灵活,用户可以快速切换分类,而不需要打开菜单。

TabBarView的内容

TabBarView根据选中的Tab显示不同的游戏列表:

          Expanded(
            child: _isLoading
                ? const LoadingWidget()
                : TabBarView(
                    controller: _tabController,
                    children: [
                      _buildGameList(_allGames),
                      _buildGameList(_pcGames),
                      _buildGameList(_browserGames),
                    ],
                  ),
          ),

TabBarView会根据_tabController的当前索引显示对应的子Widget。用户切换Tab时,TabBarView会自动切换显示的内容。

加载中时显示LoadingWidget,加载完成后显示TabBarView。

游戏列表的展示

_buildGameList方法构建游戏列表:

  Widget _buildGameList(List<dynamic> games) {
    if (games.isEmpty) return const EmptyWidget(message: '暂无游戏');
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: games.length,
      itemBuilder: (context, index) {
        final game = games[index];
        return Card(
          margin: const EdgeInsets.only(bottom: 12),
          clipBehavior: Clip.antiAlias,
          child: InkWell(
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => GameDetailScreen(gameId: game['id']))),
            child: Row(
              children: [
                AppNetworkImage(
                  imageUrl: game['thumbnail'] ?? '',
                  width: 120,
                  height: 80,
                  borderRadius: BorderRadius.zero,
                ),

每个游戏用一个Card来展示,包含游戏的缩略图、标题、描述、分类和平台。

用Row来横向排列图片和文字,这样能充分利用屏幕空间

游戏信息的展示:

                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.all(12),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(game['title'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis),
                        const SizedBox(height: 4),
                        Text(game['short_description'] ?? '', style: TextStyle(fontSize: 12, color: Colors.grey[600]), maxLines: 2, overflow: TextOverflow.ellipsis),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            TagChip(label: game['genre'] ?? '', color: Colors.blue),
                            const SizedBox(width: 8),
                            TagChip(label: game['platform'] ?? '', color: Colors.green),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),

用Expanded让文字部分占满剩余空间。Column中从上到下显示:标题、描述、分类和平台。

标题用粗体显示,描述用较小的灰色文字。分类和平台用TagChip来展示,这样看起来很整洁。

所有文字都限制了行数和overflow,防止文字超出卡片

分类筛选的设计

分类筛选是这个页面的一个重要特性。用户可以快速切换分类来查看不同类型的游戏:

final List<String> _categories = ['all', 'mmorpg', 'shooter', 'strategy', 'moba', 'racing', 'sports', 'social', 'card'];
final Map<String, String> _categoryNames = {
  'all': '全部',
  'mmorpg': 'MMORPG',
  'shooter': '射击',
  'strategy': '策略',
  'moba': 'MOBA',
  'racing': '竞速',
  'sports': '体育',
  'social': '社交',
  'card': '卡牌',
};

_categories列表定义了所有可用的分类。_categoryNames是分类的中文名称映射。

这样设计的好处是易于维护。如果以后要添加新的分类,只需要在这两个地方添加即可。

多维度筛选的实现

这个页面支持两个维度的筛选:

  1. 平台维度 - 通过Tab切换(全部、PC、浏览器)
  2. 分类维度 - 通过FilterChip切换(MMORPG、射击、策略等)

这两个维度是独立的,用户可以在任何Tab下选择任何分类。比如用户可以查看"PC上的MMORPG游戏"或"浏览器上的卡牌游戏"。

用户体验的细节

1. 快速筛选

分类筛选用FilterChip实现,用户可以快速点击来切换分类,不需要打开菜单。

2. 视觉反馈

选中的分类会用不同的样式显示,用户能清楚地看到当前选中的分类。

3. 空状态处理

如果某个分类或平台没有游戏,会显示"暂无游戏"的提示。

4. 合理的布局

游戏列表用Row来横向排列图片和文字,这样能充分利用屏幕空间,显示更多信息。

性能考虑

虽然代码看起来简单,但有几个性能相关的点:

1. 数据缓存

_allGames、_pcGames、_browserGames分别缓存三个平台的游戏。当用户切换Tab时,不需要重新加载数据。

2. 列表优化

ListView.builder是懒加载的,只有即将显示的游戏卡片才会被构建。

3. 图片优化

游戏缩略图用AppNetworkImage加载,这样能处理加载中和加载失败的情况。

可能的改进

虽然当前的实现已经很完整,但还有一些可能的改进:

1. 并行加载

当前三个API请求是串行的,可以用Future.wait改成并行加载,提升性能。

2. 分页加载

如果游戏数量很多,可以实现分页加载,而不是一次性加载所有游戏。

3. 搜索功能

可以添加搜索功能,让用户快速找到想要的游戏。

4. 排序功能

可以添加排序功能,比如按热度、按发布时间等排序。

总结

这篇文章我们实现了一个功能完整的免费游戏列表页面。涉及到的知识点包括:

  • TabController - 如何管理多个Tab
  • 多维度筛选 - 如何实现平台和分类的双维度筛选
  • FilterChip - 使用FilterChip实现快速筛选
  • 列表布局 - 用Row和Column组合实现复杂的列表项布局
  • 数据管理 - 如何缓存和管理多个数据源
  • 用户体验 - 快速筛选、视觉反馈等细节

一个好的列表页面能让用户快速找到想要的内容。通过合理的筛选设计和清晰的信息展示,我们能创造一个高效的浏览体验。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐