Flutter for OpenHarmony 万能游戏库App实战 - 首页宝可梦图鉴推荐实现

宝可梦作为全球最知名的IP之一,在游戏库App中加入宝可梦图鉴功能是个不错的选择。PokeAPI是一个完全免费、无需注册的API,提供了非常丰富的宝可梦数据。这篇文章我们来实现首页的宝可梦推荐列表,和上一篇的游戏推荐不同,这次我们会更深入地探讨图片URL拼接、并行请求、以及卡片样式的差异化设计。
请添加图片描述

从API说起

PokeAPI的设计很有意思,它不会一次性返回所有数据,而是采用分页+详情的模式。先看看获取列表的接口:

class PokemonApi {
  static const String baseUrl = 'https://pokeapi.co/api/v2';
  final ApiService _api = ApiService();

  Future<Map<String, dynamic>> getPokemonList({int offset = 0, int limit = 20}) async {
    final result = await _api.get('$baseUrl/pokemon?offset=$offset&limit=$limit');
    return result as Map<String, dynamic>;
  }

这个接口返回的是宝可梦的名称和详情URL列表,并不包含图片等详细信息。offset和limit参数用于分页,默认获取前20个。为什么这样设计?因为宝可梦有上千只,一次性返回所有详情数据量太大了。

返回的数据结构大概是这样的:

{
  "results": [
    {"name": "bulbasaur", "url": "https://pokeapi.co/api/v2/pokemon/1/"},
    {"name": "ivysaur", "url": "https://pokeapi.co/api/v2/pokemon/2/"}
  ]
}

注意这里只有name和url,没有图片地址。但我们可以根据宝可梦的ID来拼接图片URL,这是个小技巧,后面会讲到。

图片URL的拼接技巧

PokeAPI官方提供了一个GitHub仓库专门存放宝可梦图片,URL格式是固定的:

https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/{id}.png

这个地址指向的是官方高清立绘,比API返回的小图好看多了。只要知道宝可梦的ID,就能直接拼出图片地址,不需要额外请求详情接口。

在代码中是这样用的:

AppNetworkImage(
  imageUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png',
  width: 60,
  height: 60,
),

这里的$id就是宝可梦的编号。由于列表接口返回的顺序是按编号排列的,所以index + 1就是对应的ID。这样做的好处是省去了N次详情请求,首页加载速度快了很多。

并行加载多个数据源

首页不只有宝可梦,还有游戏推荐等其他内容。如果一个一个串行加载,用户要等很久。我们用Future.wait来并行加载:

Future<void> _loadData() async {
  setState(() {
    _isLoading = true;
    _gamesError = null;
    _pokemonError = null;
  });
  
  await Future.wait([
    _loadGames(),
    _loadPokemon(),
  ]);
  
  if (mounted) {
    setState(() => _isLoading = false);
  }
}

Future.wait接收一个Future列表,同时发起所有请求,等全部完成后才继续执行。这样两个接口的耗时是取最长的那个,而不是相加。比如游戏接口要2秒,宝可梦接口要1秒,总耗时是2秒而不是3秒。

加载宝可梦数据的具体实现:

Future<void> _loadPokemon() async {
  try {
    final pokemon = await _pokemonApi.getPokemonList(limit: 10);
    if (mounted) {
      setState(() {
        _featuredPokemon = pokemon['results'] ?? [];
        _pokemonError = null;
      });
    }
  } catch (e) {
    if (mounted) {
      setState(() {
        _pokemonError = e.toString();
        _featuredPokemon = [];
      });
    }
  }
}

首页只展示10个宝可梦,所以limit: 10。注意这里用了??空合并运算符,如果results为null就返回空列表,避免后续代码报空指针错误

宝可梦卡片的UI设计

宝可梦卡片和游戏卡片的设计思路不太一样。游戏卡片是横向的大图+文字,宝可梦卡片是纵向的小卡片,更紧凑:

return SizedBox(
  height: 140,
  child: ListView.builder(
    scrollDirection: Axis.horizontal,
    padding: const EdgeInsets.symmetric(horizontal: 12),
    itemCount: _featuredPokemon.length,

整个列表高度固定140,比游戏列表的200要矮。因为宝可梦卡片内容简单,不需要那么高。padding设置左右12的内边距,和游戏列表保持一致,视觉上更协调。

每个卡片的构建:

    itemBuilder: (context, index) {
      final pokemon = _featuredPokemon[index];
      final id = index + 1;
      return Container(
        width: 100,
        margin: const EdgeInsets.symmetric(horizontal: 4),
        child: Card(
          child: InkWell(
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => PokemonDetailScreen(pokemonId: id))),
            borderRadius: BorderRadius.circular(16),

卡片宽度100,比游戏卡片的260窄很多。这样一屏能显示更多宝可梦,符合图鉴浏览的使用场景。用户想快速浏览有哪些宝可梦,而不是仔细看每一个的详情。

卡片内部的布局:

            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AppNetworkImage(
                  imageUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png',
                  width: 60,
                  height: 60,
                ),
                const SizedBox(height: 8),
                Text(
                  pokemon['name'].toString().toUpperCase(),
                  style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                Text('#${id.toString().padLeft(3, '0')}', style: TextStyle(fontSize: 10, color: Colors.grey[600])),
              ],
            ),

布局很简单:图片、名称、编号,垂直居中排列。名称用toUpperCase()转成大写,这是宝可梦的传统展示方式。编号用padLeft(3, '0')补零,比如1变成001,25变成025,看起来更专业。

处理加载失败的情况

网络请求难免会失败,我们要给用户一个友好的提示和重试的机会:

if (_pokemonError != null) {
  return SizedBox(
    height: 140,
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.cloud_off, size: 40, color: Colors.grey[400]),
          const SizedBox(height: 8),
          Text(l10n.loadingFailed, style: TextStyle(color: Colors.grey[600], fontSize: 12)),

错误状态的容器高度也是140,和正常状态保持一致。这样切换状态时页面不会跳动。图标用cloud_off,一眼就能看出是网络问题。

重试按钮:

          TextButton.icon(
            onPressed: _loadPokemon,
            icon: const Icon(Icons.refresh, size: 16),
            label: Text(l10n.retry, style: const TextStyle(fontSize: 12)),
          ),
        ],
      ),
    ),
  );
}

点击重试只会重新加载宝可梦数据,不会影响其他已经加载成功的内容。这比整个页面刷新体验好多了。按钮和文字都用了较小的字号,因为这个区域本身就不大。

空数据的处理

有时候接口返回成功了,但数据是空的,这种情况也要处理:

if (_featuredPokemon.isEmpty) {
  return SizedBox(
    height: 140,
    child: Center(child: Text(l10n.noData, style: const TextStyle(color: Colors.grey))),
  );
}

这种情况比较少见,但防御性编程是个好习惯。万一API改了返回格式,或者服务器出了问题返回空数据,用户看到的是"暂无数据"而不是一片空白或者报错。

点击跳转详情页

点击宝可梦卡片会跳转到详情页,我们把ID传过去:

onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => PokemonDetailScreen(pokemonId: id))),

这里传的是数字ID而不是名称,因为后续请求详情接口用ID更方便。详情页会根据这个ID去获取完整的宝可梦信息,包括属性、技能、种族值等。

关于国际化

你可能注意到代码里用了l10n.loadingFailedl10n.retry这样的写法,这是Flutter的国际化方案:

Widget _buildPokemonList(AppLocalizations l10n) {

方法参数里传入了AppLocalizations对象,这样方法内部就能使用多语言文本了。把国际化对象作为参数传递,比在方法内部重新获取更高效,因为build方法可能会频繁调用。

和游戏推荐的对比

写到这里,我们可以对比一下宝可梦推荐和游戏推荐的异同:

相同点:

  • 都是横向滚动列表
  • 都有加载中、加载失败、空数据三种状态
  • 都支持点击跳转详情

不同点:

  • 卡片尺寸不同(100 vs 260宽度)
  • 图片来源不同(拼接URL vs API返回)
  • 展示信息不同(名称+编号 vs 名称+类型)

这种差异化设计是有意为之的。不同类型的内容用不同的展示方式,用户一眼就能区分,也不会产生视觉疲劳。

性能优化的思考

虽然这个功能看起来简单,但有几个性能相关的点值得注意:

1. 图片懒加载

ListView.builder本身就是懒加载的,只有即将显示的item才会被构建。配合网络图片组件的loading状态,用户体验很流畅。

2. 避免重复请求

我们在State里缓存了_featuredPokemon,页面rebuild时不会重新请求。只有下拉刷新或者点击重试才会重新加载。

3. 合理的数据量

首页只加载10个宝可梦,数据量很小。如果用户想看更多,可以点击"查看全部"进入完整的图鉴列表页。

写在最后

这篇文章我们实现了首页的宝可梦图鉴推荐功能。虽然代码量不大,但涉及到的知识点还挺多的:API设计理解、图片URL拼接、并行请求、差异化UI设计、错误处理等等。

做App开发就是这样,看起来简单的功能,要做好其实有很多细节要考虑。希望这篇文章对你有所帮助,有问题欢迎留言讨论。


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐